Response header
< X-Content-Type-Options: nosniffX-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.
- Direction
- Response
- Category
- Security
- Spec
- Fetch Standard
On this page
TL;DR: Send
X-Content-Type-Options: nosniffwith accurateContent-Typeheaders. 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: 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.
Syntax
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:
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:
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:
Header always set X-Content-Type-Options "nosniff"
These are documented by nginx and Apache.
For an existing Express app, put the setter before static files and API routes:
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 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 describes the distinction.
For a fixed downloadable file, Express can set the attachment disposition while you set the response type deliberately:
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 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:
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; do not wrap open() in a context manager that closes it before the streaming response is consumed.
Testing
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:
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 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:
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.
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:
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
Frequently asked questions
What is X-Content-Type-Options?
X-Content-Type-Options: nosniff prevents MIME sniffing and blocks script-like or stylesheet responses with incompatible Content-Type values. It does not sanitize content.
Why should I use nosniff?
nosniff prevents a response from being reinterpreted as a different type and enforces MIME checks for scripts and stylesheets. It complements accurate Content-Type headers.
Does nosniff have any downsides?
Incorrect or missing Content-Type values can cause scripts and stylesheets to fail. Fix the server response rather than disabling the header.
Should I always use X-Content-Type-Options: nosniff?
Use nosniff alongside accurate Content-Type headers. It is not a substitute for validating uploads or isolating untrusted HTML.
Sources
Related
X-Powered-By Header: Remove It in Express, PHP, Next.js
X-Powered-By advertises your framework, such as Express or PHP. Remove it in Express, PHP, Next.js, ASP.NET and nginx, and why it is hygiene, not security.
MIME Type (Media Type)
A MIME type, or media type, is the type/subtype label in Content-Type that tells clients how to interpret a body. Covers nosniff and blocked-script errors.
Access-Control-Allow-Credentials Header
Learn how Access-Control-Allow-Credentials controls whether browsers expose responses when credentials (cookies, auth headers) are included in CORS requests.
Access-Control-Allow-Headers Header
Learn how Access-Control-Allow-Headers specifies which custom HTTP headers can be used during cross-origin requests in CORS preflight responses.