Zend PHP 7 Certification – Security – File Uploads

This post covers the File Uploads section of the PHP Security chapter when studying for the Zend PHP 7 Certification.

File uploads made via the $_FILES superglobal is filled with user-supplied data and therefore can pose a security risk as the user-supplied data can never be trusted.

The first security risk is that file names can be forged, and so correct validation should be used to prevent your system from being compromised.

PHP provides a couple of functions which offer protection against this kind of attack, along with a configuration file directive known as open_basedir that limits the files that can be accessed by PHP to the specified directory-tree.

The realpath() and basename() functions are the two that PHP provides to help avoid directory traversal attacks.

The realpath() function translates any . or .. in a path, resulting in the correct absolute path for a file.

<?php
chdir('/var/www/');
echo realpath('./../../etc/passwd') . PHP_EOL;

echo realpath('/tmp/') . PHP_EOL;

// Outputs:
/etc/passwd
/tmp

The basename() function strips the directory part of a name, leaving behind just the filename itself.

Using these two functions, it is possible to rewrite the script above in a much more secure manner.

<?php
echo "1) ".basename("/etc/sudoers.d", ".d").PHP_EOL;
echo "2) ".basename("/etc/sudoers.d").PHP_EOL;
echo "3) ".basename("/etc/passwd").PHP_EOL;
echo "4) ".basename("/etc/").PHP_EOL;
echo "5) ".basename(".").PHP_EOL;
echo "6) ".basename("/");

// Outputs:
1) sudoers
2) sudoers.d
3) passwd
4) etc
5) .
6) 

File mime types can be forged, so it is recommended that you create an allowed list of file mime types.

$finfo = new finfo(FILEINFO_MIME_TYPE);
if (false === $ext = array_search(
    $finfo->file($_FILES['upfile']['tmp_name']),
    array(
        'jpg' => 'image/jpeg',
        'png' => 'image/png',
        'gif' => 'image/gif',
    ),
    true
)) {
    throw new RuntimeException('Invalid file format.');
}

The $_FILES tmp_name can also be forged, so it is usually advised to rename the uploaded file to a safe unique name, such as in the example below using move_uploaded_file().

if (!move_uploaded_file(
    $_FILES['upfile']['tmp_name'],
    sprintf('./uploads/%s.%s',
        sha1_file($_FILES['upfile']['tmp_name']),
        $ext
    )
)) {
    throw new RuntimeException('Failed to move uploaded file.');
}

You can then use is_uploaded_file() to verify whether the file was uploaded via HTTP POST.

if (is_uploaded_file($_FILES['upfile']['tmp_name'])) {
   echo "File ". $_FILES['upfile']['name'] ." uploaded successfully.\n";
   echo "Displaying contents\n";
   readfile($_FILES['upfile']['tmp_name']);
} else {
   echo "Possible file upload attack: ";
   echo "filename '". $_FILES['upfile']['tmp_name'] . "'.";
}

View the other sections:

Note: This article is based on PHP version 7.0.