# X-Content-Type-Options (/en/docs/web-security/security-headers/x-content-type-options)



Browsers used to inspect the first bytes of a response and guess its real
type whenever the declared `Content-Type` was missing or looked wrong, a
behavior called [MIME sniffing](https://mimesniff.spec.whatwg.org/).
`X-Content-Type-Options: nosniff` turns the guessing off: the browser trusts
the type your server declares and treats the response as that type only.

The guessing was the attack surface: a file uploaded as a harmless image
that really contains HTML and JavaScript could be sniffed into a page and
executed in your site's origin, a stored XSS. This header closes that path.

This is the whole header. It only does its job when the `Content-Type` on
each response is correct:

```http
X-Content-Type-Options: nosniff
```

## Values and what each does [#values-and-what-each-does]

| Value     | Status | Description                                                                                                                  |
| --------- | ------ | ---------------------------------------------------------------------------------------------------------------------------- |
| `nosniff` | ✅ Good | The only value. The browser trusts the declared `Content-Type` and blocks scripts and stylesheets whose type does not match. |

`nosniff`, defined in the
[Fetch standard](https://fetch.spec.whatwg.org/#x-content-type-options-header),
has two effects.

First, a hard block for scripts and stylesheets. Any script-like load (a
`<script>`, a worker, a shared worker, a service worker, an audio or paint
worklet) must arrive with a JavaScript MIME type, and a stylesheet must
arrive as exactly `text/css`. On a mismatch the browser blocks the response
at the network level and it never executes; Chromium logs a console error
saying the script was refused because "strict MIME type checking is
enabled". Without the header, Chromium still executes scripts served as
`text/plain`, `text/html`, or `application/octet-stream` (image, audio,
video, and CSV types are blocked as scripts either way).

Second, no byte guessing anywhere else. When a response carries a
`Content-Type`, the declared type is final. When it carries none, the
browser may still classify the body as text or binary, but it will never
sniff it toward HTML, XML, or PDF.

Chrome also ships [Opaque Response Blocking (ORB)](https://github.com/annevk/orb),
which blocks cross-origin no-cors reads of responses that look like HTML,
JSON, or XML for every site, whatever headers it sends. `nosniff` makes that
blocking deterministic: a blocklisted, `text/plain`, or missing type fails
closed instead of depending on confirmation sniffing. ORB does nothing for
same-origin loads, so the header remains the only thing enforcing MIME
correctness for your own scripts and stylesheets.

## The Content-Type side [#the-content-type-side]

The header enforces the label, so the label has to be right on every
resource: scripts served with a JavaScript MIME type (`text/javascript`),
stylesheets as `text/css`, HTML as `text/html`. On HTML responses, declare
the character encoding too:

```http
Content-Type: text/html; charset=UTF-8
```

The [OWASP HTTP headers cheat sheet](https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html)
puts it plainly: "the charset attribute is necessary to prevent XSS in HTML
pages". The classic version of that attack, smuggling script through UTF-7,
is dead because browsers dropped the encoding, but omitting the charset is
still exploitable.
[Research published by Sonar in 2024](https://www.sonarsource.com/blog/encoding-differentials-why-charset-matters/)
showed that when a page omits its charset, Chrome and Firefox auto-detect
ISO-2022-JP, and an attacker who controls part of the page can use that
encoding's escape sequences to break out of escaping mid-document, a working
XSS in 2024. Declare the charset on every HTML response.

## What it protects against [#what-it-protects-against]

* **Mislabeled uploads executing as HTML or script.** An `avatar.png` that
  is really an HTML file, served with a missing or unknown type, could be
  sniffed into a page and run in your origin as stored XSS. With `nosniff`
  the browser takes the file at its declared type.
* **Misconfigured and type-stripping infrastructure.** A server that omits
  `Content-Type`, or a proxy that strips it, leaves the browser to guess.
  The header removes the guess, so a configuration slip stays a broken
  resource instead of becoming executable content.
* **Mislabeled worker scripts.** The hard block covers workers, shared
  workers, service workers, and worklets, so a resource pulled in as a
  worker must carry a JavaScript MIME type too.
* **Cross-origin data leaks.** It strengthens the browser's cross-origin
  read blocking (ORB) against Spectre-class attacks by making the block
  deterministic instead of sniffing-based.

The header enforces the label and nothing more. A response correctly
labeled `text/html` still renders, correctly labeled JavaScript still runs,
so it does not replace
[Content Security Policy (CSP)](/en/docs/web-security/policies/content-security-policy/introduction/what-is-csp)
or upload handling (validation, a separate origin for user content).

## Risks without it [#risks-without-it]

Without the header, every response needs a perfect `Content-Type`, because
any response without one is open to reinterpretation. One user-content
route that serves files with a missing or unknown type, one proxy that
strips the header, and a sniffing engine can promote a harmless-looking
file to HTML in your origin. Modern engines have narrowed sniffing (they
only sniff toward HTML when the type is missing or unknown), so the
practical exposure concentrates in misconfigured surfaces and legacy
engines, which is exactly what defense in depth is for.

## Risks and gotchas when using it [#risks-and-gotchas-when-using-it]

* **Wrongly typed scripts stop loading.** A legitimate script served as
  `text/plain` or `application/octet-stream` (a bare file server, an
  [Amazon S3](https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingMetadata.html)
  object uploaded without a type) is blocked the moment `nosniff` ships.
  Fix the MIME mappings before adding the header, not after.
* **Stylesheets must be exactly `text/css`.** Anything else is blocked, the
  same way scripts require a JavaScript MIME type.
* **Header form only.** There is no `<meta>` equivalent; it must be sent as
  a response header.
* **No violation reports.** Blocked loads surface as console errors only.
  Unlike CSP, the header has no Reporting API integration, so nothing tells
  you a resource broke in the field; test before and after deploying.

## How to set it up [#how-to-set-it-up]

1. Audit your `Content-Type` mappings: every script served with a
   JavaScript MIME type, every stylesheet as `text/css`, every HTML
   response as `text/html; charset=UTF-8`. Pay attention to file servers
   and object storage, where the type is whatever was set at upload.
2. Add `X-Content-Type-Options: nosniff` to every response, API responses
   included. Setting it once at the server or CDN level is easier than
   per route, and there is no value to tune.
3. Check the deployed header, and the rest of your response headers, with
   the [security headers scanner](/tools/security-headers), and
   watch the browser console for refused scripts or stylesheets after the
   rollout.

## Recommendation [#recommendation]

Send `X-Content-Type-Options: nosniff` on every response:

```http
X-Content-Type-Options: nosniff
```

The [OWASP HTTP headers cheat sheet](https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html)
recommends sending it on all responses browsers may consume, and the
[OWASP REST security cheat sheet](https://cheatsheetseries.owasp.org/cheatsheets/REST_Security_Cheat_Sheet.html)
extends that to API responses. Pair it with a correct `Content-Type` on
every resource and a charset on HTML (`text/html; charset=UTF-8`): the
header enforces the label, so the label has to be right.

## Browser support [#browser-support]

Supported in every modern engine. Microsoft introduced the header in
[Internet Explorer in 2008](https://learn.microsoft.com/en-us/archive/blogs/ie/ie8-security-part-vi-beta-2-update);
their demo was an HTML file served as `text/plain` that rendered as HTML in
older Internet Explorer but as plain text in the version that shipped
`nosniff`. Chrome, Firefox, and Safari all enforce it fully in current
versions (some early Chrome builds did not enforce the stylesheet check).
Firefox also applies `nosniff` to top-level page loads (see
[Mozilla's announcement](https://blog.mozilla.org/security/2020/04/07/firefox-75-will-respect-nosniff-for-page-loads/)):
a navigation whose declared type does not match shows an error page, and a
missing type renders as plain text or downloads.

## FAQ [#faq]

### Do I need nosniff on API responses? [#do-i-need-nosniff-on-api-responses]

Yes. The OWASP REST security cheat sheet recommends
`X-Content-Type-Options: nosniff` on every response, API responses included.
It costs nothing to send site-wide, stops a JSON response from being sniffed
toward HTML, and there is no value to tune, so set it once at the server or
CDN level rather than per route.

### Does nosniff break anything? [#does-nosniff-break-anything]

Only resources sent with the wrong `Content-Type`. A script served as
`text/plain`, or a stylesheet that is not exactly `text/css`, gets blocked the
moment the header ships. Fix your MIME mappings first, then add `nosniff`, so
correctly labeled content keeps loading and only the previously mislabeled
resources surface as errors.

### Does nosniff stop XSS? [#does-nosniff-stop-xss]

No. It closes one specific path: a mislabeled upload being sniffed and executed
as HTML or script in your origin. Correctly labeled HTML still renders and
correctly labeled JavaScript still runs, so you still need Content Security
Policy and proper upload handling. Treat `nosniff` as defense in depth, not an
XSS fix.

## See also [#see-also]

* [Security headers overview](/en/docs/web-security/security-headers)
* [What is Content Security Policy](/en/docs/web-security/policies/content-security-policy/introduction/what-is-csp),
  which restricts what correctly labeled content can do; `nosniff` only
  enforces the labels
* [Strict-Transport-Security](/en/docs/web-security/security-headers/strict-transport-security),
  the transport-layer sibling in this section
* [Security headers scanner](/tools/security-headers) to check your
  deployed headers

## Sources [#sources]

* [Fetch standard, X-Content-Type-Options header](https://fetch.spec.whatwg.org/#x-content-type-options-header)
* [MIME Sniffing standard](https://mimesniff.spec.whatwg.org/)
* [MDN, X-Content-Type-Options](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Content-Type-Options)
* [OWASP, HTTP headers cheat sheet](https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html)
* [Sonar, encoding differentials, why charset matters](https://www.sonarsource.com/blog/encoding-differentials-why-charset-matters/)
* [Microsoft, IE8 security part VI, the nosniff announcement](https://learn.microsoft.com/en-us/archive/blogs/ie/ie8-security-part-vi-beta-2-update)
