Introduction
File upload vulnerabilities arise when a system allows users to upload files in an unsafe manner. These vulnerabilities can be exploited regardless of whether the attacker can subsequently invoke the uploaded file. For example, a system that fails to validate the size of uploaded files may be vulnerable to a denial-of-service attack if an attacker uploads a file large enough to exhaust the available disk space.
How File Uploads Work
The way a server handles uploaded files depends on its configuration:
- If the file type is non-executable (e.g., an image or a static HTML page), the server sends the file contents to the client.
- If the file type is executable (e.g., PHP) and the server is configured to execute that type, the file will be executed server-side.
- If the file type is executable but the server is not configured to execute it, the server generally returns an error.
HTTP Request Structure
File uploads are transmitted using multipart/form-data requests. The Content-Type header specifies both the type and a boundary string that separates the form fields:
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryqQFv421UZxV128wE
Each field in the request body is delimited by this boundary. For example:
------WebKitFormBoundaryqQFv421UZxV128wE
Content-Disposition: form-data; name="avatar"; filename="temp_shell.php"
Content-Type: application/x-php
<?php echo file_get_contents('/home/carlos/secret'); ?>
------WebKitFormBoundaryqQFv421UZxV128wE
Content-Disposition: form-data; name="user"
wiener
------WebKitFormBoundaryqQFv421UZxV128wE
Content-Disposition: form-data; name="csrf"
kys6Cdo1AVseVTWNyx9yt2Nn3Kfjcp4N
------WebKitFormBoundaryqQFv421UZxV128wE--
The boundary is randomly generated by the browser and separates the individual form fields. Everything within a boundary section belongs to one field.
Note: The
Content-Typeresponse header may reveal what the server believes it has served. If not explicitly set, the response type will reflect the extension-to-MIME mapping.
Bypassing File Upload Defenses
Applications commonly implement restrictions on file uploads, but each restriction must be tested thoroughly.
Content-Type Validation
An application that trusts the Content-Type header supplied by the client can be trivially bypassed, as this header is easily spoofed. Testing should be performed with a valid-seeming MIME type (e.g., image/jpeg) while sending a malicious payload.
Path Traversal in Filenames
A server may be configured to prevent execution of files within the upload directory (e.g., /user-profile-pics/). However, if it does not validate the filename and is vulnerable to path traversal, an attacker can escape the protected directory by uploading a file named:
../../payload.php
This writes the file outside the sandboxed directory, where execution may be permitted.
Extension Blacklist Bypasses
Blacklist-based extension filtering is fragile. Many bypass techniques exist:
Overlooked extensions: A blacklist may block .php but not .php5, .phtml, .pht, or other PHP-related extensions. Similarly, .html may be blocked while .shtml is not.
Server configuration manipulation: An attacker can alter how the server interprets extensions by uploading a configuration file first. For example, uploading an .htaccess file containing:
AddType application/x-httpd-php .l33t
Then uploading a payload with the .l33t extension, which is not blacklisted. The same technique applies to web.config on IIS or httpd.conf/apache2.conf if writable.
Case variations: If the blacklist is case-sensitive and blocks .php, variations such as .PhP or .pHp can be attempted.
Null byte and delimiter injection: If the filename is processed by low-level C/C++ functions, a null byte (%00) or semicolons can be used to terminate the filename early:
exploit.asp;.jpg
exploit.asp%00.jpg
The server sees the allowed extension, but the actual stored file may have a different (or truncated) extension.
Recursive stripping bypass: If the defense strips blacklisted extensions from the filename, nesting can defeat it:
exploit.p.phphp
After stripping .php, the remaining characters collapse into exploit.php.
Apache extension mapping: When Apache encounters an unrecognized extension, it walks left through the filename until it finds one it knows how to handle. A file named exploit.php.jpg may be interpreted as PHP if .jpg is not mapped to a handler; Apache falls back to .php.
Polyglot Shells
A polyglot file is simultaneously valid in two formats (e.g., JPEG and PHP). This can bypass content-type checks that inspect the file’s magic bytes. They can be created using ExifTool:
exiftool -Comment="<?php echo 'START ' . file_get_contents('/home/carlos/secret') . ' END'; ?>" input.jpg -o polyglot.php
The resulting file is both a valid image and a valid PHP script.
Race Conditions
Some systems upload the file temporarily to disk before running validation checks. The file exists for a short window during which an attacker can execute it. This is a race condition. For example:
<?php
$target_dir = "avatars/";
$target_file = $target_dir . $_FILES["avatar"]["name"];
// temporary move
move_uploaded_file($_FILES["avatar"]["tmp_name"], $target_file);
if (checkViruses($target_file) && checkFileType($target_file)) {
echo "The file " . htmlspecialchars($target_file) . " has been uploaded.";
} else {
unlink($target_file);
echo "Sorry, there was an error uploading your file.";
http_response_code(403);
}
?>
If the server uses randomly generated filenames, an attacker can extend the processing time (e.g., by uploading a larger file) to increase the window for brute-forcing the directory and filename.
PUT Requests
Some servers support PUT requests for file uploads. This can serve as an alternative attack vector when the application’s upload functionality is well-defended.
Alternative Attack Vectors
Even when server-side code execution is not possible, file uploads can enable other attacks:
- Client-side injection: An attacker can upload HTML or JavaScript files that execute in the browsers of users who visit the uploaded content.
- XXE injection: Files parsed server-side, such as
.docor.xlsdocuments, may be vulnerable to XML External Entity injection if the server processes them through an XML parser.
Remediation
- Check file extensions against a whitelist of permitted types. Never rely on a blacklist.
- Do not trust the
Content-Typeheader supplied by the client. Validate the file type by inspecting its actual contents (magic bytes) rather than the declared MIME type. - Set a filename length limit and restrict allowed characters where possible.
- Set a file size limit to prevent disk-exhaustion DoS attacks.
- Store uploaded files on a separate server. If that is not possible, store them outside the web root. When public access is required, use an application-layer handler that maps internal identifiers to filenames (e.g.,
?file=4839→file.ext). - Ensure filenames do not contain path traversal sequences (
../) and are properly sanitized before filesystem operations. - Rename uploaded files to avoid collisions and overwrites. Use randomly generated names rather than preserving user-supplied filenames.
- Do not write files to the server’s filesystem until they are fully validated, to prevent race condition exploitation.
- Use established frameworks that provide built-in secure file upload handling.