PHP / SQL Security – Direct File Uploads

File Uploads

File uploads can occur as part of a multi-part HTTP POST request. PHP provides ways to process these file uploads in a secure manner, including checking to make sure the file you’re operating on was in fact an uploaded file. There are several security issues with file uploads which should be addressed when designing secure PHP sites.

In the file-upload procedure, the filename as determined by the web browser is passed to the web server, and thus to the PHP script. The filename supplied by the browser is part of the submitted data, which may be under the control of an attacker and therefore this filename should be distrusted wherever it is possible to do so.

Consider the following case as an example. A PHP script which moves files to the location reported by $_FILES[‘file’][‘name’] receives an upload from a browser which told the web server the file it had just uploaded was /home/andrew/.bashrc

Normally, .bashrc is a file which is associated with the UNIX Bash shell, and contains commands executed by Bash every time a Bash shell is started. These commands clearly run as the user who invoked the shell, and have all of the privileges and permissions of that user. If PHP has write access to their home directory, using the name of an uploaded file as it was supplied would allow for an attacker to position a carefully prepared .bashrc file with a single POST request. This file might then open a terminal session piped over a network port, or run some kind of exploit or root kit, or worse!

In order to prevent this kind of attack, we must take heed of the advice from part three of this series, stripping the filename down to remove the path data, and distrust the browser-supplied filename. In doing so, it is ideally best to create unique file names locally, perhaps based on the current time, or some unique sequence stored in a (secure) database, and to use those unique names as the actual on-disk filename. In order to avoid confusing end-users, it is possible to map the real (unique) filename to the browser-supplied filename by means of a database, and the browser-supplied name can be used for all interaction with the user, whereas the unique, locally generated filename, will be used for any server-side file operations.

If such a mechanism is not practical, for whatever reason, the precautions from part three should be followed, and the browser-supplied filename should be expanded to an absolute path using the realpath() function, and then the file name part only obtained with the basename() function. realpath() translates any . (which refers to the current directory) or .. (which refers to the parent directory) in a path, resulting in the correct absolute path for a file. basename() strips the directory part of a name, leaving behind just the filename itself. This sanitised filename should then be reasonably safe to use directly with the file functions of PHP.

However, if an attacker somehow managed to learn your directory structure, then they may be able to overwrite other files in the directory into which you place any uploaded files, by providing an upload with the same name as an existing one, or with the same name as one of your PHP scripts, which may then get included into another script and executed, or executed directly by web access to that script, if it is in a location accessible to the web server. The script execution scenario represents a very clear security threat, as has been explained in the previous parts of this series, but many more subtle security issues can occur as a result of replacement of a variety of system files, files PHP or the web server rely on, or files used by your web application itself.

To maintain the best security, locally generated and unique filenames should be preferred over the browser-supplied ones, and checks for the existence of a file should be made prior to moving an uploaded file into a directory, so as to prevent accidental (or intentional!) overwriting of files already on the server.

File uploads can be turned off altogether if there is no reason for your web application to accept uploaded files. This may be achieved by setting the following directive in php.ini

file_uploads = Off

When file uploading is turned on, it is possible for the drive to become filled by repeated uploads or by large files being uploaded. PHP provides a mechanism to limit the length of any uploaded files, preventing the upload of files larger than this size, but you would have to perform checks yourself to make sure that the disk being used as the destination for these uploaded files contains enough space that the file upload will not cause the free space to go below a critical amount required for the functioning of the system. If the web server, or any other service on the system, cannot create the files it needs to perform its duty, because uploaded files have filled the available drive space, this is a form of Denial of Service attack.

Individual POST requests can be limited in size using the following directive in the php.ini file

post_max_size = 8M

Where the 8M sets an 8MB limit for the entire POST request. Note that file uploads make up only a part of the multi-part HTTP POST request, and that if multiple files are uploaded, the sum of their sizes forms the total file upload size, which is only one part of the POST request size.

To control file upload size specifically, you can use the following php.ini directive

upload_max_filesize = 2M

Where 2M specifies a 2MB filesize limit. Once again, note that this is the total file size for all files included in the POST request, and not a per-file limit. The upload_max_filesize should be slightly smaller than the post_max_size because the POST request will contain other data, headers and form fields, beyond the file data itself.

The default post_max_size is 10MB, which is much larger than most sites require. Processing a POST request takes time, so limiting the size of the request prevents an attacker from initiating several large POST requests which would use up resources on the server and deny service to other users. Setting this value to a lower value, around 2MB for sites which require small file uploads, or under 1MB for sites which do not, should improve the responsiveness of the server if it is under attack.

Uploaded files are moved to a temporary directory, since they are processed by the web server itself, before PHP can see them. The default location for temporary files is the system temporary file directory, which is usually defined to be /tmp on a UNIX system. This temporary file directory is often readable by all users, and therefore storing uploaded files here, even temporarily, is not good security, since any user with access to the system is likely to have access to the uploaded file data, between the time it was uploaded and the time that a PHP script moves the file into its final destination.

It is considered good practice to change the directory used for uploading temporary files to one which is owned by the user under which the web server (and, consequently, PHP) runs, and prevent other users accessing this directory. The following line in php.ini tells PHP to use a different location for temporary storage of uploaded files.

upload_tmp_dir = /var/www/tmp

You can change /var/www/tmp to a different directory, suitable for your server layout, and create it using the following

cd /var/www
mkdir tmp
chown httpd tmp

where httpd is the username of the user account under which the web server runs.

When dealing with uploaded files, it is essential to know that the file you are performing file operations on was, in fact, an uploaded file. It is possible to trick PHP into operating on a file which was not actually uploaded, by providing an incorrect filename, or exploiting some other vulnerability in the web application. To make absolutely certain that you are operating on a file which was indeed uploaded, PHP provides two functions. is_uploaded_file() returns true only if the filename it was given was actually uploaded, and move_uploaded_file() performs a file move operation only if the filename was in fact an uploaded file. Combining these two functions is much safer than using the standard file manipulation functions such as copy().

$supplied_name = $_FILES['file']['name'];
$temp_name = $_FILES['file']['tmp_nam'];
$count++; // Persistent counter to uniquely identify files
$local_name = "file_$count";
if( is_uploaded_file($temp_name) )
move_uploaded_file($temp_name, "/home/files/$local_name");
echo "File $supplied_name successfully uploaded.";
die("Error processing the file");

The script above combines some of the advice of the above sections. A locally generated unique name is used for storing the files on the filesystem, the is_uploaded_file() and move_uploaded_file() functions are used to ensure that the file being operated on was an uploaded file, and an attacker did not trick us into moving some system or other important file into a location from which the web server can access it directly, and the browser-supplied filename is displayed to the user for consistency.

The example could have been greatly improved; for example, checking that the free disk space is not below a certain level before moving the file into it, so as to prevent filling the drive, or storing a mapping of local unique name to browser-supplied name in a database.

As a final word on file uploads, it is often a good idea to store uploaded files outside of the web server’s document tree, even if these files are to be retrieved later. It is possible to create a PHP script, download.php, which takes a filename in a GET request and uses readfile() to send the file to the user, creating the appropriate headers for length and content-type. This is much safer than allowing direct download, especially of user-uploaded files, since the script can perform additional checking to make sure that the requested file is one which should be downloadable, and can also perform other housekeeping such as tracking download counts, or imposing limitations. Allowing downloads through the web server directly eliminates much of this security and functionality.

How to check for PHP vulnerabilities

The best way to check whether your web site & applications are vulnerable to PHP security attacks is by using a Web Vulnerability Scanner. A Web Vulnerability Scanner crawls your entire website and automatically checks for vulnerabilities to PHP attacks. It will indicate which scripts are vulnerable so that you can fix the vulnerability easily. Besides PHP security vulnerabilities, a web application scanner will also check for SQL injection, Cross site scripting & other web vulnerabilities.

Click here to read part five.