Working with Files: Sanitizing Externally Supplied Filenames in PHP

Just as data submitted in a form or URL can cause problems when it is displayed (cross-site scripting attack) or put in an SQL query (SQL injection attack), it can also cause problems when it is used as a filename or as part of a filename. This problem doesn’t have a fancy name like those other attacks, but it can be just as devastating.

The cause of the problem is the same: there are special characters that must be esca­ped so they lose their special meaning. In filenames, the special characters are / (which separates parts of filenames), and the two-character sequence .. (which means “go up one directory” in a filename).

For example, the funny-looking filename /usr/local/data/../../../etc/passwd doesn’t point to a file in the /usr/local/data directory but instead to the location of the file /etc/passwd, which, on most Unix systems, contains a list of user accounts. The filename /usr/local/data/../../../etc/passwd means “from the directory /usr/local/ data, go up one level (to /usr/local), then go up another level (to /usr), then go up another level (to /, the top level of the filesystem), then down into /etc, then stop at the file passwd.”

How could this be a problem in your PHP programs? When you use data from a form in a filename, you are vulnerable to an attack that enables a user to gain access to areas of your filesystem that you may not have intended, unless you sanitize the submitted form data. Example 9-21 takes the approach of removing all forward slashes and .. sequences from a submitted form parameter before incorporating the parameter into a filename.

Example 9-21. Cleaning up a form parameter that goes in a filename

// Remove slashes from user

$user = str_replace(‘/’, ”, $_POST[‘user’]);

// Remove .. from user

$user = str_replace(‘..’, ”, $user);

if (is_readable(“/usr/local/data/$user”)) {

print ‘User profile for ‘ . htmlentities($user) .’: <br/>’;

print file_get_contents(“/usr/local/data/$user”);

}

If a malicious user supplies ../../../etc/passwd as the user form parameter in Example 9-21, that is translated into etcpasswd before being interpolated into the filename used with file_get_contents().

Another helpful technique for getting rid of user-entered nastiness is to use realpath(). It translates an obfuscated filename that contains .. sequences into the .. -less version of the filename that more directly indicates where the file is. For example, realpath(‘/usr/local/data/../../../etc/passwd’) returns the string /etc/passwd. You can use realpath() as in Example 9-22 to see whether file­names, after incorporating form data, are acceptable.

Example 9-22. Cleaning up a filename with realpath()

$filename = realpath(“/usr/local/data/$_POST[user]”);

// Make sure that $filename is under /usr/local/data if ((‘/usr/local/data/’ == substr($filename, 0, 16))

&& is_readable($filename)) {

print ‘User profile for ‘ . htmlentities($_POST[‘user’]) .’: <br/>’;

print file_get_contents($filename);

} else {

print “Invalid user entered.”;

}

In Example 9-22, if $_POST[‘user’] is james, then $filename is set to /usr/local/data/james and the if() code block runs. However, if $_POST[‘user’] is something suspicious such as ../secrets.txt, then $filename is set to /usr/local/ secrets.txt, and the if() test fails, so Invalid user entered. is printed.

Source: Sklar David (2016), Learning PHP: A Gentle Introduction to the Web’s Most Popular Language, O’Reilly Media; 1st edition.

Leave a Reply

Your email address will not be published. Required fields are marked *