X-Content-Type-Options
A user uploads a file to your site. You serve it back with Content-Type: text/plain. If the file happens to contain something that looks like HTML, some browsers will look at the bytes, decide "this is actually HTML," and render it. Any script or event handler inside runs with your origin's cookies.
That is the MIME-sniffing problem. The fix is a one-line header that tells the browser to trust the Content-Type you sent and stop second-guessing.
The problem
Historical browsers treated Content-Type as a hint, not a contract. The reasoning was pragmatic: plenty of early web servers shipped everything as text/plain or even no type at all. Sniffing kept those sites working. It also created a persistent class of bugs where user-uploaded content escaped the type the server declared.
- 01user uploads comment.txt containing <img src=x onerror=steal()>
- 02server serves it as Content-Type: text/plain
- 03browser peeks at the bytes, guesses "HTML"
- 04inline script executes as same-origin
- 01user uploads comment.txt containing <img src=x onerror=steal()>
- 02server serves it as text/plain AND sends X-Content-Type-Options: nosniff
- 03browser trusts the Content-Type, renders as text
- 04bytes display literally, no code runs
The attack class is called "MIME confusion." User uploads anywhere (avatars, attachments, issue comments) can trigger it. The patch is the shortest security header in the catalogue.
The fix
X-Content-Type-Options: nosniffOne name, one value, one behaviour. Introduced by Microsoft for IE 8 in 2008, then adopted by every other browser. Formally specified today in the WHATWG Fetch standard §3.6. Any value other than nosniff is ignored.
What it changes
For script and style destinations, the browser goes further than disabling sniffing: it refuses the response outright if the MIME does not match. A stylesheet served as text/html will not load. A script served as text/plain will not execute. That is stricter than old-style sniffing would have allowed; the intent is to close the last remaining gap.
For everything else (HTML, images, JSON, files in downloads), the rule is simpler: trust the Content-Type the server sent, do not peek at the bytes.
Where to ship it
Send this header on every response your server serves. Not just HTML pages. Every JSON endpoint, every image, every downloadable file, every static asset. The misconfigurations this header catches are by definition unpredictable.
Most modern frameworks set it by default. Nginx, Apache, Caddy, and every CDN expose a one-line config to enable it globally. If you have a single place that could set one security header, make it this one.
Common mistakes
Every byte served by your origin is a potential MIME-confusion vector. Ship nosniff site-wide.
X-Content-Type-Options: yes or nosniff; mode=block or similar extensions are silently ignored. The only valid value is the exact string nosniff.
nosniff makes the browser trust your Content-Type. If your server sends text/plain for actual JavaScript, nosniff will refuse to load it. Fix both.
A CDN serving JavaScript from /static/ also needs the header. Set it at the edge or in every origin response.
Even with nosniff, user content served from the same origin as the app is risky. Prefer a separate uploads subdomain or a CDN with Content-Disposition: attachment and restrictive CSP.
Check whether your site ships nosniff on every response: scan your domain.