content-type-mime-diff · git:20260504.2a64374 · 2026-05-04 · sha256 7799a52a5bf57157
content-type-mime-diff git:20260504.2a64374A
Immutable. This exact content is served forever at /api/v1/blob/7799a52a5bf57157.
---
name: content-type-mime-diff
description: MIME Content-Type parser differentials between browsers and server-side libraries for XSS. Browsers treat Content-Type as list-based (last value wins), while server-side parsers treat it as singleton (first value wins). Use when application validates Content-Type server-side but reflects it verbatim in response headers.
---
# Content-Type MIME Parser Differential -> XSS
## Root Cause
- RFC 9110 defines `Content-Type` as a singleton field
- Browsers such as Chrome and Firefox treat it as list-based: split on comma and take the last valid MIME type
- Server-side MIME parsers usually take the first MIME type and ignore the rest
- If the application validates the parsed type but sets the original unparsed value in the response header, the parser differential becomes exploitable
## Core Payload Pattern
```
application/json;,text/html
```
- Server-side parser sees `application/json`
- Browser sees `application/json;` then `text/html` and renders as HTML
## Library-Specific Payloads
### Python `email.message`
```
application/json;,text/html
```
### Python `googleapiclient.mimeparse`
```
application/json;=,text/html
```
or:
```
application/json;,text/html,=
```
### Strict parsers
Parsers like Express.js `content-type`, `busboy`, and `rigour.mime` may throw exceptions on invalid characters. In those cases, exploitability depends on error handling:
- if the application falls back unsafely, the bug may still be reachable
- if the application returns the original header value anyway, the differential still matters
### Chromium parenthesis quirk
```
application/json;,text/html(=
```
Chrome treats `(` as a MIME comment delimiter and still resolves `text/html`.
## Vulnerable Code Pattern
```python
# DANGEROUS: validates parsed value but uses original in response
parsed = parse_mime(user_input)
if parsed != "application/json":
return error
response.headers["Content-Type"] = user_input
```
Always emit the normalized parsed MIME type, never the original attacker-controlled value.
## Parser Behavior Summary
| Parser | Payload | Result |
|--------|---------|--------|
| Chrome/Firefox | `app/json;,text/html` | `text/html` |
| Python `email.message` | `app/json;,text/html` | `application/json` |
| Python `werkzeug` | `app/json;,text/html` | `application/json` |
| Python `cgi` | `app/json;,text/html` | `application/json` |
| Python `googleapiclient` | `app/json;=,text/html` | `application/json` |
| Node `whatwg-mimetype` | `app/json;,text/html` | `application/json` |
| Node `content-type` | `app/json;,text/html` | Exception |
| PHP `fileeye/mimemap` | `app/json;,text/html` | `application/json` |
## Testing Steps
1. Find an endpoint where you control the response `Content-Type` value.
2. Confirm whether `X-Content-Type-Options: nosniff` is present.
3. Test `application/json;,text/html` and observe whether the browser renders the response as HTML.
4. If validation exists, identify the server-side parser and adjust the payload accordingly.
5. Combine with HTML or JavaScript in the body to confirm XSS impact.
## Indicators
- Parameters such as `type`, `content-type`, or `format` influence response MIME type
- `X-Content-Type-Options: nosniff` is present, so browser behavior depends on the explicit MIME type
- The server validates MIME types but reflects the original input into the response header
## Chain With
- `crlf-response-splitting`
- `web-cache-deception-path`
- `parser-differential-bypass`
## References
- https://lab.ctbb.show/research/parse-and-parse-mime-validation-bypass-to-xss-via-parser-differential
- https://github.com/BlackFan/content-type-research