Skip to main content
All Posts

The File Upload That Looked Safe — Until I Changed One Request Header

4 min read
CybersecurityPenetration TestingWeb SecurityOWASPBurp SuiteFile UploadMSc Cybersecurity
The File Upload That Looked Safe — Until I Changed One Request Header

The file upload form had validation. It checked the file type, rejected anything that wasn't an image, and showed a helpful error message. The check ran entirely in the browser.

The file upload form had validation. It checked the file type, rejected anything that wasn't an image, and showed a helpful error message when you tried to upload a .php file. The check ran entirely in the browser.

Once I intercepted the request with Burp Suite and changed one header, I had a shell on the server.


What Client-Side Validation Actually Does

Client-side validation runs in the browser before the request is sent. It can check the file extension, the declared MIME type, even the file size — and it can reject the upload before anything reaches the server.

What it cannot do is stop someone from modifying the request after it leaves the browser.

Burp Suite sits between the browser and the server as a proxy. When you intercept a request, you can read and modify every field before it is forwarded. The browser has already done its checks. The server receives whatever you send.

The distinction matters: client-side validation is a UX feature. It saves valid users from uploading the wrong thing accidentally. It is not a security control.


The Bypass

The upload form accepted image files. When I uploaded a PHP file directly, the browser rejected it before the request was sent. So I uploaded a legitimate image first, intercepted the request in Burp Suite, and replaced the file content with a PHP web shell while keeping the original Content-Type: image/jpeg header.

The request the server received looked like an image upload. The content was:

<?php system($_GET['cmd']); ?>

The server had no validation of its own. It accepted the declared MIME type at face value, stored the file, and served it back at a predictable URL under the web root.


What Remote Code Execution Looks Like

Once the file was stored and accessible, I could execute arbitrary commands on the server by passing them as a URL parameter:

http://target/uploads/shell.php?cmd=whoami

The server returned the current user context. From there: directory listing, reading configuration files, extracting database credentials, writing new files. The upload form was the entry point. The impact extended to everything the web server process could reach.

This is the difference between a vulnerability and a finding. A file upload form with client-side-only validation is a low-severity observation until you demonstrate that it leads to remote code execution — then it becomes a critical finding.


Why This Pattern Persists

File upload functionality is common and the vulnerable pattern is easy to introduce. A developer adds a file type check to the front-end form, confirms it works in the browser, and ships it. The check is real — it does work for legitimate users. The gap is that server-side validation is a separate concern that the front-end check doesn't address.

OWASP lists unrestricted file upload consistently in its top vulnerability categories. It persists not because developers don't know about it, but because the front-end validation gives a false sense that the control is in place.


The Fix

Three controls close this vulnerability:

Server-side MIME validation. The server checks the actual content of the uploaded file — not the declared Content-Type header, which the client controls — and rejects anything that doesn't match the expected file type. Libraries exist for this in every major language.

File extension whitelist. Permitted extensions are defined explicitly on the server. Anything not on the list is rejected before the file is stored, regardless of what the Content-Type header says.

Upload directory outside the web root. Even if a malicious file is stored, it cannot be executed if it is not accessible via the web server. Moving the upload directory outside the document root means stored files can be served to users through a controlled endpoint but cannot be accessed directly as executables.

Each control independently reduces the attack surface. Together they eliminate the bypass entirely.


The Takeaway

A security control that exists only in the browser is not a security control. It is a convenience feature that can be bypassed by anyone with a proxy and ten minutes.

When assessing upload functionality during a penetration test, the question is never "does the form reject bad files?" — the browser can be bypassed. The question is "does the server reject bad files?" If the answer requires checking the browser's behaviour to verify, the control is in the wrong place.


This assessment was part of my MSc Cybersecurity at Robert Gordon University. Full project write-up: Web Application Penetration Testing & Hardening