showerhead and haylem covered this question quite thoroughly, but there's a few more things I think should be added or expounded upon.
Be extremely careful when including based on user input
This was mentioned a few times by both people mentioned, but considering how big of security hole it can be, I feel that it deserves a lot more attention than anyone gave it.
You should NEVER include a file blindly based on user input.
What if I go to: page.php?module=../some/secret
Suddenly /path/some/secret.php
just got included. The .php
extension would limit a lot of the damage this could do (page.php?module=../path/to/some/config.ini
), but the hole is still there.
I tend to be extremely strict with file name handling. I typically only allow a-z
, A-Z
, 0-9
, -
, and _
. That way you know that it will be a semi-logical file name, and you know that it will not be able to traverse directories.
For example:
$module = (isset($_GET['module']) && is_string($_GET['module'])) ? $_GET['module'] : null;
if (!preg_match('/^[a-zA-Z0-9_-]$/', $module)) {
$module = false;
}
You should also check if the file is readable before including it. Otherwise, a user may see an ugly error message, or, best case, you open up the possibility of a huge log file of meaningless errors. (Production servers should never have display_errors = On
, but they should have error_reporting = E_ALL
.)
Don't assume (user provided) array keys are set
showerhead touched on this:
Also, I would not immediately use any value from any user provided array directly in my program. It should be sanitized and validated first.
But I think one of the reasons behind this should be explained a bit more.
When you access an array, if the key does not exist, then a notice is triggered about the array key not existing. As such, you should always make sure you are 100% certain that an array key exists. The only way to do this on user controlled inputs is explicity checked. The filter_input method is a favorite of many, but if you wanted to it "raw", you can use isset
.
$val = (isset($_GET['val'])) ? $_GET['val'] : null;
It's guaranteed that every value of a $_GET/$_POST/etc array will be either a string or an array, thus it's a good idea to check type too. Using an array as a string will trigger quite a few errors in most scripts.
$val = (isset($_GET['val']) && is_string($_GET['val'])) ? $_GET['val'] : null;
array_key_exists
is worth mentioning here too. It does basically the same thing, except it can be used when a value in an array may be false.
$a = array('foo' => null);
if (isset($a)) { echo 'isset'; }
if (array_key_exists('foo', $a)) { echo 'exists'; }
exists
will echo here, but isset
will not since isset(null) === false
.
Separation of concerns
showerhead explained the concept well, but I disagree with his execution a bit.
As part of having only one responsibility, a function should have no inner depencies. What I mean by this is that a function that is not related to direct database interaction should not even have to know that a database exists. (Obviously at some point in your code, this will have to be broken, but it's more a general idea than a code-law.)
In more technically, slightly more vague terms, if a function has any dependencies, they should be provided to a function rather than a function finding those dependencies itself. ("dependency injection" is typically what this is called.)
I would consider rewriting your code like:
function output(array $recordsA, array $recordsB, $authorized, $module)
{
if($authorized)
{
include('/path/to/' . $module . '.php');
}
echo "<ul>"
foreach ($recordsA as $record) {
echo "<li>" . htmlspecialchars($row['description']) . "</li>";
}
echo "</ul><br><ul>";
foreach ($recordsB as $record) {
$status = ($authorized) ? htmlspecialchars($record['status']) : 'N/A';
$description = htmlspecialchars($record['description']);
echo "<li>{$description} ({$status})</li>";
}
echo "</ul>";
}
Then the usage of the function would be:
$recordsAQuery = mysql_query("...");
$recordsA = array();
while ($row = mysql_fetch_assoc($recordsAQuery)) {
$recordsA[] = $row;
}
$recordsBQuery = mysql_query("...");
...
$module = ...;
output($recordsA, $recordsB, is_authorized(), $module);
I would also be tempted to abstract yet more. Specifically, the database stuff could be abstracted away a bit more depending on how carried away one was.
function queryAndFetchAll($query, $conn = null)
{
$res = mysql_query($query);
if (!$res) {
throw new Exception("mysql_query failed. Query: [{$query}]. Error: [" . mysql_error($conn) . "]");
}
$records = array();
while ($row = mysql_fetch_assoc($res)) {
$records[] = $row;
}
return $records;
}
$recordsA = queryAndFetchAll("SELECT ...");
$recordsB = queryAndFetchAll("SELECT ...");
Note that I would name recordsA and recordsB something more descriptive. (I also might consider breaking this function into two functions.)
And, depending on what exactly the module file is doing, I might try to abstract that out of the function.
On a side note, the PDOStatement is Iterable, so you can pass it and the function can use it silently as an array.
If you're not familiar with PDO this probably won't have much meaning, but hopefully the gist is there:
$recAStmt = $db->prepare("SELECT * FROM main WHERE id > ?");
$recAStmt->execute(array($id));
$recBstmt = ...;
output($recAStmt, $recBStmt, ...);
output
What is being output? I would consider naming this something like renderStatuses
. (Though if you were to rename it render*, I would expect it to return content instead of echoing.)
echo vs return
That brings me to my next point :).
I avoid outputting to the client except for in my main view code. What if for some reason you want to store this code to a variable instead of echoing it immediately? If you want to echo immediately, it's easy enough to do echo f();
, but storing and then echoing is quite a pain. (ob_start(); f(); $f = ob_get_flush();
) This does tend to make functions take a bit longer to write, but it's typically worth it.
SELECT *
This one is being pretty picky, however, I favor explicitly listing the column name. There's a tiny performance benefit (even if selecting all columns), but more so than that, I would rather my queries break in the future than my PHP code silently mess up.
Consider if you decided to rename your status column. Your code would silently break. You'd start getting undefined index notices, and nothing would be output correctly.
However, if you had selected all of the columns individually, the query would fail, and you'd likely notice the error a lot quicker.
include
is open to cross directory including, you don't check that any of the keys are set in your array accesses, and you don't escape your HTML output. \$\endgroup\$