How to check X-Content-Type-Options
Send a HEAD request and read one header: curl -sI https://example.org/ | grep -i x-content-type-options. A server that blocks MIME sniffing answers x-content-type-options: nosniff. Empty output means the header is absent, and the browser is then free to guess the type of a response and run a text file as script.
Why check this
MIME sniffing is the browser reading the first bytes of a response and overriding the content type the server declared. With nosniff the browser stops guessing and honours the declared type for scripts and stylesheets. Without it, a file stored as text/plain that begins with HTML markup can be rendered as HTML on your own origin.
Run the check on every route that returns a file a user supplied: avatar uploads, attachment downloads, exported reports, generated invoices. Run it again after a CDN or a storage bucket goes in front of those routes, because the header now comes from whichever layer writes the response. The homepage almost always carries nosniff. The download route added three sprints later usually does not, and step 3 finds that gap.
Prerequisites
- curl 7.0 or later. Any build works, no HTTP/2 support is needed. See the curl manual.
- The MDN page on X-Content-Type-Options for the two request destinations the header constrains.
- Node 22 for steps 3 and 4. Save this as
upload-server.mjsand start it withnode upload-server.mjs. It imitates an app whose HTML route sets the header and whose file route does not.
import { createServer } from 'node:http';
const upload = Buffer.from('<script>document.title="sniffed"</script>');
createServer((req, res) => {
if (req.url === '/') {
res.writeHead(200, { 'content-type': 'text/html', 'x-content-type-options': 'nosniff' });
res.end('<h1>home</h1>');
} else if (req.url === '/files/notes.txt') {
res.writeHead(200, { 'content-type': 'text/plain' });
res.end(upload);
} else {
res.writeHead(404).end();
}
}).listen(8080, () => console.log('listening on 8080'));
Steps
- Step 1.
Read the header on a site that sets it, together with the content type it protects.
curl -sI https://www.cloudflare.com/ | grep -iE '^HTTP|^content-type|^x-content-type-options'HTTP/2 103 HTTP/2 200 content-type: text/html; charset=utf-8 x-content-type-options: nosniffTwo status lines appear because this host sends a 103 early hints response before the 200. Read the headers that follow the last status line.
- Step 2.
Count the header on a site that omits it, so that absence is a number and not an empty screen.
curl -sI https://example.com/ | grep -ci 'x-content-type-options'0 - Step 3.
Start the local server, then read the headers of the file route rather than the homepage.
curl -sD - -o /dev/null http://localhost:8080/files/notes.txt | grep -iE '^HTTP|content-type|x-content-type-options'HTTP/1.1 200 OK content-type: text/plainThe homepage of the same server answers
x-content-type-options: nosniff. The file route does not, which is the finding. - Step 4.
Print the body of that file route to see what a browser would be sniffing.
curl -s http://localhost:8080/files/notes.txt<script>document.title="sniffed"</script>Declared
text/plain, actual content HTML, nonosniff. Stop the server with Ctrl+C once you have this line.
How to read the result
| What you see | What it means | What to do |
| --- | --- | --- |
| x-content-type-options: nosniff | The browser honours the declared type on this route | Nothing. Repeat on the upload and download routes. |
| Nothing at all | Sniffing is allowed on this response | Add the header at the layer that writes the response, then re-run step 2. |
| nosniff on HTML, absent under /files/ | The header is set on one handler, not globally | Move it to the shared response middleware instead of to each route. |
| x-content-type-options: NOSNIFF | Still valid, the token is case insensitive | Nothing. Match with grep -i so your test does not report a false miss. |
Common mistakes
What to check next
- How to check security headers: reads this header together with the others in one request.
- How to check CSP header: the policy that limits what a sniffed script could reach if it ever executes.
- How to check X-Frame-Options: the other single-token header set in the same middleware.
- How to check if mixed content exists on a page: another way a page loads content the origin did not intend.
- Security headers checker: paste a URL and see which of these headers a host returns.
FAQ
Is any value other than nosniff valid?
No. nosniff is the only token the specification defines. Anything else is ignored, so a typo such as no-sniff leaves sniffing enabled while the header still looks present in a scan report.
Does the header protect every response type?
It constrains two request destinations, scripts and stylesheets. For those, a wrong declared type makes the browser refuse the resource. Browsers also apply it to downloads, which stops a file from being displayed inline as HTML.
Can I check it in Chrome DevTools instead?
Open DevTools, Network tab, click the request, then the Headers panel and read Response Headers. That works for one request. The curl form in step 3 is what you put in a test that runs on every build.
Why does the check fail only on staging?
The header is usually added by a proxy or a CDN rule that exists in one environment and not the other. Compare both environments with step 1 before you edit any application code.
Verified
Verified by Maks Vernycurl 8.21.0node 22.23.2
Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.
Related on this site
basic5 minpublished updated Maks Verny