# X-Content-Type-Options Header

> Learn how X-Content-Type-Options with nosniff prevents browsers from MIME-sniffing responses. Protect against XSS attacks from content type confusion.

Source: https://howhttpworks.com/headers/x-content-type-options
Last reviewed: 2026-10-05

> **TL;DR:** Send `X-Content-Type-Options: nosniff` with accurate `Content-Type` headers. It blocks scripts and stylesheets served with incompatible MIME types and prevents document MIME sniffing. It does not sanitize HTML or validate uploads.

## What is X-Content-Type-Options?

This response header tells the browser to respect the declared media type. Its most visible failure is a script or stylesheet refusing to load after a server returns the wrong type. Fix the response type or routing error rather than removing the header.

## How X-Content-Type-Options Works

[Fetch defines a blocking check](https://fetch.spec.whatwg.org/#x-content-type-options-header): a script-like destination needs a JavaScript MIME type; a style destination needs `text/css`. With `nosniff`, a missing or incompatible type fails that check.

For document navigations, `nosniff` prevents reinterpreting non-HTML content as HTML. Without the header, behavior depends on the supplied type and loading context. A `text/plain` response is not automatically executable HTML. See the [MIME Sniffing Standard](https://mimesniff.spec.whatwg.org/#identifying-a-resource-with-an-unknown-mime-type).

## Syntax

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

There are no configurable MIME lists. The token is matched without regard to case, although lowercase `nosniff` is the conventional spelling.

## Common Examples

Illustrative header excerpt for an external JavaScript file:

```http
Content-Type: text/javascript; charset=utf-8
X-Content-Type-Options: nosniff
```

For a stylesheet, use `Content-Type: text/css`. A filename ending in `.js` or `.css` does not repair an incorrect response header.

Typical response types differ by resource:

| Resource | Content-Type |
| --- | --- |
| JavaScript file | `text/javascript` |
| Stylesheet | `text/css` |
| JSON API response | `application/json` |
| Plain-text download | `text/plain` |

Add `nosniff` alongside the type, rather than changing every response to `application/octet-stream`. That generic binary type is unsuitable for a stylesheet or a script the page must load.

## Real-World Scenarios

A missing `/assets/app.js` can accidentally hit the application's HTML fallback. The browser receives `text/html` instead of JavaScript and rejects it. Check the response body as well as the status: an HTML error page can arrive with `200 OK` if the fallback handler returns success.

## Attack Scenarios Prevented

A plain-text upload should remain plain text when opened as a document. However, an uploaded HTML document served intentionally as `text/html` is still HTML with `nosniff`. The header does not remove scripts from it or isolate it from the site's origin.

Nor does Fetch's script/style blocking check constitute a general image or PDF safety check. Those formats need their own content handling controls.

## Getting X-Content-Type-Options right

In an existing nginx `server` block:

```nginx
add_header X-Content-Type-Options "nosniff" always;
```

`always` includes error responses. Check locations that define their own `add_header` directives because inheritance can differ. With Apache's `mod_headers` loaded:

```apacheconf
Header always set X-Content-Type-Options "nosniff"
```

These are documented by [nginx](https://nginx.org/en/docs/http/ngx_http_headers_module.html#add_header) and [Apache](https://httpd.apache.org/docs/2.4/mod/mod_headers.html#header).

For an existing Express app, put the setter before static files and API routes:

```javascript
const express = require('express')
app.use((_req, res, next) => {
  res.set('X-Content-Type-Options', 'nosniff')
  next()
})
app.use('/assets', express.static('/srv/app/public/assets', { fallthrough: false }))
app.get('/api/status', (_req, res) => res.json({ status: 'ok' }))
```

[Express static serving](https://expressjs.com/en/5x/api/express/#express.static) handles file types, while `res.json()` declares JSON. `fallthrough: false` passes missing assets to error handling instead of an ordinary next route, helping keep `/assets/missing.js` out of the SPA's HTML fallback. Keep error handlers after these routes so the same header reaches their responses.

## MIME Type Validation

A browser-supplied upload MIME type is a claim from the client, not inspection of the file bytes. `req.file.mimetype` describes the submitted type; it does not prove what the file contains. Choose response types from trusted application knowledge and separately validate allowed uploads; [OWASP's upload guidance](https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html#content-type-validation) describes the distinction.

For a fixed downloadable file, Express can set the attachment disposition while you set the response type deliberately:

```javascript
app.get('/downloads/report', (_req, res) => {
  res.download('/srv/app/reports/report.txt', 'report.txt', {
    headers: {
      'Content-Type': 'text/plain; charset=utf-8',
      'X-Content-Type-Options': 'nosniff'
    }
  })
})
```

[res.download](https://expressjs.com/en/5x/api/response/#res.download) serves an attachment. This fixed path avoids trusting a filename supplied by the caller; protect the route with authentication if the report is private. Attachment disposition and nosniff solve different problems.

For generated bytes, set `res.attachment('report.txt')`, then `res.type('text/plain')` before `res.send(buffer)`. The disposition helper can infer a type from the filename, so make the intended type explicit after it.

Django's FileResponse supports the same separation:

```python
from django.http import FileResponse

def report(request):
    response = FileResponse(
        open("/srv/app/reports/report.txt", "rb"),
        as_attachment=True,
        filename="report.txt",
        content_type="text/plain; charset=utf-8",
    )
    response["X-Content-Type-Options"] = "nosniff"
    return response
```

[FileResponse closes the file automatically](https://docs.djangoproject.com/en/5.2/ref/request-response/#fileresponse-objects); do not wrap `open()` in a context manager that closes it before the streaming response is consumed.

## Testing

```bash
curl -sS -D - -o /dev/null https://app.example.com/assets/app.js
```

Verify the final response has both headers. Then load the resource through a browser script or stylesheet element and inspect Network and Console. A successful curl download does not prove the browser accepts the MIME type.

To reproduce a script MIME failure locally, add these routes to an existing Express test app:

```javascript
app.get('/mime-test', (_req, res) => {
  res.type('html').send('<script src="/wrong-type.js"></script>')
})
app.get('/wrong-type.js', (_req, res) => {
  res.set('X-Content-Type-Options', 'nosniff')
  res.type('text/plain').send('console.log("loaded")')
})
```

Open `/mime-test`. The classic script has a plain-text response and is blocked by the MIME check; `loaded` should not be logged. Inspect its response headers, then change the type to `text/javascript` and reload to confirm that the same script body can load.

Use [MDN's compatibility data](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Content-Type-Options#browser_compatibility) for specific older browser targets. Header presence alone is not evidence that every client enforces every loading-context rule.

The same response can still be read as text by a same-origin fetch:

```javascript
const response = await fetch('/wrong-type.js')
console.log(await response.text())
```

That request has no script destination. Its successful download does not contradict the script element's MIME rejection; the [Fetch check depends on the request destination](https://fetch.spec.whatwg.org/#should-response-to-request-be-blocked-due-to-nosniff).

## Common Patterns

Set the header centrally, then keep asset MIME types correct. For an API returning JSON, declare `application/json`; adding `nosniff` does not grant CORS permission or let the response execute as a script.

For generated text bytes, the attachment pattern is:

```javascript
app.get('/downloads/generated-report', (_req, res) => {
  const report = Buffer.from('status: ok\n', 'utf8')
  res.set('X-Content-Type-Options', 'nosniff')
  res.attachment('report.txt')
  res.type('text/plain').send(report)
})
```

The bytes and filename here are fixed application values. The response offers a text attachment, not an HTML document. If real reports contain user input, keep the download type and disposition deliberate; nosniff does not escape HTML inserted into a separate preview page.

## Impact on Script/Style Loading

Classic scripts, module scripts, and stylesheets also have other loading checks. Passing the `nosniff` test does not bypass CSP, CORS, or module MIME requirements. Avoid diagnosing every blocked asset as this header's fault.

For a stylesheet, a `200 OK` JSON error response is still the wrong type. If `/assets/theme.css` redirects to a sign-in page, inspect that final HTML response. Fixing the asset URL or authentication routing is necessary; adding another nosniff field cannot turn the returned HTML into CSS.

## Common Errors and Solutions

When Console reports a MIME mismatch, inspect the requested URL, redirect chain, `Content-Type`, and response body. Repair an HTML fallback or error response before changing headers. Serving an HTML body as JavaScript would conceal the routing problem and still produce a JavaScript parse failure.

## Related Headers

- [Content-Type](https://howhttpworks.com/headers/content-type)
- [Content-Security-Policy](https://howhttpworks.com/headers/content-security-policy)
- [X-XSS-Protection](https://howhttpworks.com/headers/x-xss-protection)
