Before we can exploit file upload vulnerabilities, we need to understand exactly how file uploads work at the HTTP level. In the previous lesson, we set up our lab environment — now let's dissect the mechanics of a file upload from the moment a user selects a file to the moment the server processes it.
At its core, a file upload is an HTTP POST request with a special content type called multipart/form-data. This encoding type allows the request body to contain multiple parts — each representing a form field — separated by a boundary string. When a file is included, one of these parts contains the raw binary data of the file along with metadata such as the filename and content type.
A typical file upload form in HTML looks like this:
<form action="/upload.php" method="POST" enctype="multipart/form-data">
<input type="file" name="uploaded_file" id="fileInput" />
<input type="submit" value="Upload" />
</form>The critical attribute here is enctype="multipart/form-data". Without it, the browser would encode the request as application/x-www-form-urlencoded, which cannot handle binary file data. The type="file" input renders a file picker dialog that allows the user to select a file from their local filesystem.
When the user submits the form, the browser constructs an HTTP POST request that looks something like this:
POST /upload.php HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Length: 342
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="uploaded_file"; filename="photo.jpg"
Content-Type: image/jpeg
<binary file data here>
------WebKitFormBoundary7MA4YWxkTrZu0gW--Let's break down the key components of this request. The boundary string separates each part of the multipart message. Each part has a Content-Disposition header that includes the field name and the original filename. The Content-Type header within each part indicates the MIME type of the file — in this case, image/jpeg. The actual file data follows as raw binary content.
💡 The filename and Content-Type in the upload request are client-supplied values. This is a critical security insight: the server should never trust these values because an attacker can modify them freely using a proxy like Burp Suite.
Once the server receives the upload request, it typically goes through several processing stages. Understanding this pipeline is essential because each stage represents a potential point of failure that an attacker can exploit.
| Stage | Description | Common Vulnerabilities |
|---|---|---|
| 1. Reception | Web server receives the request and passes it to the application | Request size limits, DoS via oversized uploads |
| 2. Validation | Application checks file type, size, name, name, content | Bypassable checks, logic flaws, incomplete validation |
| 3. Sanitization | Application renames, resizes, or processes the file | Insufficient sanitization, race conditions |
| 4. Storage | File is saved to a designated location on the filesystem | Path traversal, overwriting critical files |
| 5. Serving | File is made accessible to users via URL | Execution in web root, MIME-type confusion |
Here is a minimal PHP script that handles a file upload. This example is intentionally simple and contains several vulnerabilities that we will explore throughout the course:
<?php
$upload_dir = "/var/www/html/uploads/";
$filename = basename($_FILES["uploaded_file"]["name"]);
$target_path = $upload_dir . $filename;
if (move_uploaded_file($_FILES["uploaded_file"]["tmp_name"], $target_path)) {
echo "File uploaded successfully to: " . $target_path;
} else {
echo "Upload failed.";
}
?>⚠️ This PHP script has NO validation whatsoever. It accepts any file, preserves the original filename, and places it directly in a web-accessible directory. An attacker could upload a PHP web shell and execute it by visiting the URL. This is a textbook example of an insecure file upload handler.
Notice that the script uses $_FILES, which is PHP's superglobal array containing all information about the uploaded file. The tmp_name index contains the temporary path where PHP stored the uploaded file on the server. The move_uploaded_file() function moves it to the final destination. The critical question is: what validation happens between receiving the file and moving it to its final location?
Several HTTP headers play important roles in file upload security. Understanding these headers helps both attackers and defenders:
| Header | Purpose | Security Relevance |
|---|---|---|
| Content-Type | MIME type of the overall request or individual part | Can be spoofed; server should verify independently |
| Content-Length | Size of the request body in bytes | Server should enforce maximum size limits |
| Content-Disposition | Specifies field name and filename | Filename can contain path traversal sequences |
| X-Content-Type-Options | Prevents MIME-type sniffing (nosniff) | Defensive header that prevents browser MIME confusion |
| Content-Security-Policy | Restricts what resources the browser can load | Can prevent execution of uploaded scripts in some contexts |
The fundamental principle of file upload security is: never trust client-supplied data. Every piece of metadata in the upload request — filename, MIME type, even the file extension — can be controlled by the attacker. All security decisions must be made server-side using data the attacker cannot manipulate.
Verify exercises to earn ★ 120 XP and unlock next lab level.