PHP 8.1: $_FILES: New full_path value for directory-uploads

Version8.1
TypeNew Feature

$_FILES is a PHP super-global and variable. It contains names, sizes, and mime types of files uploaded in the current HTTP request.

In HTML file upload fields, it is possible to upload an entire directory with the webkitdirectory attribute. This feature is supported in most modern browsers.

<form action="" method="post" enctype="multipart/form-data">
    <input name="myupload[]" type="file" webkitdirectory multiple />
    <input type="submit" />
</form>

Prior to PHP 8.1, the $_FILES did contain the relative paths in the user file-system to account for sub directories containing files with the same name.

For example, if the user uploads the foo directory, the $_FILES array name value only contains the file name without the relative path.

Example directory structure:

foo
 ├── dir1
 │    └── file.txt
 └── dir2
      └── file.txt

$_FILES array of the POST request:

var_dump($_FILES);
array(1) {
  ["myupload"]=> array(6) {
    ["name"]=> array(2) {
      [0]=> string(8) "file.txt"
      [1]=> string(8) "file.txt"
    }
    ["tmp_name"]=> array(2) {
      [0]=> string(14) "/tmp/phpV1J3EM"
      [1]=> string(14) "/tmp/phpzBmAkT"
    }
    // ... + error, type, size
  }
}

Prior to PHP 8.1, it was not possible accept a directory as a file upload from the browser/user-agent, and store them with exact directory structure, or access the relative paths because PHP did not pass that information to the $_FILES array.

In PHP 8.1, the $_FILES also contains a new key named full_path, that contains the full path as submitted by the browser.

var_dump($_FILES);
array(1) {
  ["myupload"]=> array(6) {
    ["name"]=> array(2) {
      [0]=> string(8) "file.txt"
      [1]=> string(8) "file.txt"
    }
+    ["full_path"]=> array(2) {
+      [0]=> string(19) "foo/test1/file.txt"
+      [1]=> string(19) "foo/test2/file.txt"
+    }
    ["tmp_name"]=> array(2) {
      [0]=> string(14) "/tmp/phpV1J3EM"
      [1]=> string(14) "/tmp/phpzBmAkT"
    }
    // ... + error, type, size
  }
}

With the full_path information, it is possible to store the relative paths, or reconstruct the same directory in the server.

In the example above, $_FILES['myupload']['full_path'] array contains the path to the uploaded file, including its path. This is useful in contrast to the $_FILES['myupload']['name'] array that contains duplicate entries because both dir1 and dir2 contain a file with the same name file.txt.

  • The temporary path for uploaded files ($_FILES['myupload']['tmp_name']) continue to be unique, and is not stored in nested directories. It is safe to continue to use the tmp_name values with the move_uploaded_file function.
  • full_path array will be for all file uploads, including standard individual/multiple file uploads. It will be identical to the name array in those cases.

Security Hardening

PHP only parses the relative path information submitted by the browser/user-agent, and passes that information to the $_FILES array. There is no guarantee that the values in the full_path array contains a real directory structure, and the PHP applications must not trust this information.

Values in full_path array are user-input, and must never be trusted.

Applications that accept a directory upload, and store the files as per the full_path values must ensure the full_path information is properly validated prior to moving the uploaded files to a nested directory.

Some of the threat models include:

  • Directory traversal attacks, by submitting a path that traverses to a different directory. e.g. foo/../../../etc/password.
  • Resource exhaustion attacks, by submitting a path with deeply nested paths. e.g. foo/a/a/a/a/a/a/a/a/a/a/a/a/a.jpg, or a very long name that would trigger path limits on certain file systems.
  • Integrity corruption by submitting a file name that may not be created otherwise. e.g. CON is not an allowed file name Windows file systems.

To prevent such attacks, always make sure to limit the nesting level, ensure the file does not exist before writing, and disallow paths that allow directory traversal. Further, standard validations against the file contents (such as inspecting the file extension and mime types) must not be removed.

Backwards Compatibility Impact

full_path array key in the $_FILES super-global is a new feature in PHP 8.1. The existing name array values are not modified, and they continue to only contain the file name without the file path.

Using full_path in older PHP versions

Unfortunately, there is no easy way to bring that functionality to older PHP versions.

HTML file uploads require enctype="multipart/form-data" attribute present in the form. With this attribute present, PHP's built-in php://input stream will be empty because PHP itself populates the $_POST and $_FILES super-globals and empties the php://input stream. This rules out fetching the browser-provided file path information by reading the php://input stream.

However, the PHP INI directive enable_post_data_reading=0 disables PHP from populating $_POST and $_FILES super-globals, thus leaving the php://input stream values untouched. As a far-fetched and strong discouraged approach, it is possible to parser the raw HTTP request and populate the $_POST and $_FILES array or PSR-7 HTTP Request objects. This information contains the browser-provided relative path to the file.

Some of the multi-part parsers include:

Alternately, it might be more straight-forward to progressively enhance user experience on web browsers by using a client-side JavaScript approach to submit the file contents and file paths in a separate field.


Implementation