HTTP security headers are your first line of defense against common web attacks. They tell browsers how to handle your content — preventing clickjacking, XSS, data sniffing, and protocol downgrade attacks.
Yet in our analysis of over 10,000 scanned websites, 78% are missing at least one critical security header, and 34% have no Content-Security-Policy at all. The good news: security headers are free, require no code changes to your application logic, and can be deployed in minutes.
Here's every security header you should set, what it does, the recommended configuration, and how to deploy it on every major platform.
Security Header Priority Table
Not all headers carry equal weight. Here's how to prioritize your implementation:
| Header | Severity | Priority | Attack Prevented | |--------|----------|----------|-----------------| | Content-Security-Policy | Critical | P0 — Deploy first | XSS, clickjacking, data injection | | Strict-Transport-Security | Critical | P0 — Deploy first | Protocol downgrade, cookie hijacking | | X-Content-Type-Options | High | P1 — Deploy same day | MIME sniffing attacks | | X-Frame-Options | High | P1 — Deploy same day | Clickjacking | | Referrer-Policy | Medium | P2 — Deploy this week | URL path/token leakage | | Permissions-Policy | Medium | P2 — Deploy this week | Feature abuse (camera, mic) | | Cross-Origin-Opener-Policy | Medium | P3 — Deploy this sprint | Spectre-like side-channel attacks | | Cross-Origin-Resource-Policy | Low | P3 — Deploy this sprint | Cross-origin resource theft |
Start with CSP and HSTS. These two headers alone block the majority of header-related vulnerabilities. Then work down the list.
Content-Security-Policy (CSP)
CSP is the most powerful security header. It controls which resources (scripts, styles, images, fonts) the browser is allowed to load. When configured correctly, it effectively eliminates reflected and stored XSS attacks.
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self' https://api.yourdomain.com; frame-ancestors 'none'
What it prevents:
- Cross-site scripting (XSS) by blocking inline scripts and unauthorized script sources
- Clickjacking via
frame-ancestors 'none' - Data exfiltration by restricting
connect-src
Common mistake: Using 'unsafe-inline' for scripts. If you must (some frameworks require it), consider adding 'strict-dynamic' with nonces. But never use 'unsafe-eval' in production.
CSP Directive Reference
Each directive controls a specific resource type:
| Directive | Controls | Recommended Value |
|-----------|----------|-------------------|
| default-src | Fallback for all resource types | 'self' |
| script-src | JavaScript execution | 'self' (add nonces if possible) |
| style-src | CSS stylesheets | 'self' 'unsafe-inline' (many frameworks need inline styles) |
| img-src | Image loading | 'self' data: https: |
| font-src | Font files | 'self' (add CDN origins if needed) |
| connect-src | XHR, fetch, WebSocket | 'self' plus your API domains |
| frame-ancestors | Who can embed your page | 'none' (or 'self' if you use iframes) |
| base-uri | Restricts <base> element | 'self' |
| form-action | Form submission targets | 'self' |
| object-src | Plugins (Flash, Java) | 'none' |
| upgrade-insecure-requests | Auto-upgrade HTTP to HTTPS | Include in every policy |
CSP Report-Only Mode
Deploying CSP directly on a live site can break things. That's why Content-Security-Policy-Report-Only exists. It logs violations without enforcing the policy:
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; report-uri /api/security/csp-report; report-to csp-endpoint
How to use it:
- Deploy your intended CSP as
Content-Security-Policy-Report-Only - Set up a
report-uriendpoint to collect violation reports - Monitor for 1-2 weeks to catch anything your policy would block
- Review the violations — each one is either a legitimate resource you need to allow, or an attack vector you're about to block
- Adjust the policy, then switch from
Report-Onlyto enforcedContent-Security-Policy
This approach lets you tighten security iteratively without breaking your site in production.
Common CSP Mistakes
Mistake 1: Using 'unsafe-inline' and 'unsafe-eval' together.
This effectively disables CSP's XSS protection. If you need inline scripts, use nonces instead:
Content-Security-Policy: script-src 'nonce-abc123random'
Then add nonce="abc123random" to each <script> tag. The nonce must be unique per request.
Mistake 2: Wildcard sources.
script-src * or script-src https: allows loading scripts from any HTTPS origin — including attacker-controlled domains. Always list explicit origins.
Mistake 3: Forgetting frame-ancestors.
Without frame-ancestors, your site can be embedded in iframes on malicious sites. This is the modern replacement for X-Frame-Options and should always be included.
Mistake 4: Using 'strict-dynamic' without nonces.
'strict-dynamic' tells the browser to trust scripts loaded by already-trusted scripts. But if you haven't established trust via nonces or hashes, 'strict-dynamic' does nothing useful — and it nullifies 'unsafe-inline' and 'self' in CSP3 browsers, potentially breaking all your JavaScript.
Mistake 5: Not including upgrade-insecure-requests.
Without this directive, mixed content (HTTP resources on HTTPS pages) will be blocked or show warnings. Adding upgrade-insecure-requests automatically rewrites HTTP URLs to HTTPS.
Strict-Transport-Security (HSTS)
Forces browsers to always use HTTPS, even if the user types http://.
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
max-age=31536000— Remember for 1 yearincludeSubDomains— Apply to all subdomainspreload— Submit to browser preload lists for HTTPS-only access
Important: Only add preload if ALL your subdomains support HTTPS. It's very hard to undo.
Why it matters: Without HSTS, an attacker on the same network (coffee shop Wi-Fi, hotel network) can intercept the initial HTTP request before the 301 redirect to HTTPS. This is called an SSL stripping attack. HSTS prevents this by telling the browser to never even attempt an HTTP connection.
X-Frame-Options
Prevents your page from being embedded in iframes on other sites (clickjacking protection).
X-Frame-Options: DENY
Options: DENY (no framing), SAMEORIGIN (only same domain). While CSP's frame-ancestors is the modern replacement, X-Frame-Options provides backward compatibility.
When to use SAMEORIGIN instead of DENY: If your application embeds its own pages in iframes (such as a help widget or preview panel), use SAMEORIGIN. Otherwise, always use DENY. For a deeper understanding of how CORS misconfigurations relate to framing attacks, see our dedicated guide.
X-Content-Type-Options
Prevents browsers from MIME-type sniffing — treating a file as a different content type than declared.
X-Content-Type-Options: nosniff
Always set this. There's no reason not to. It prevents attacks where a text file is interpreted as JavaScript.
Referrer-Policy
Controls how much referrer information is sent when navigating away from your site.
Referrer-Policy: strict-origin-when-cross-origin
This sends the full referrer for same-origin requests but only the origin for cross-origin requests. Prevents leaking URL paths (which may contain tokens or user data).
Permissions-Policy
Controls which browser features (camera, microphone, geolocation) your site can use.
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
Empty parentheses () disable the feature entirely. Only enable features your app actually uses.
Full list of commonly restricted features:
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=(), magnetometer=(), gyroscope=(), accelerometer=(), autoplay=(self), fullscreen=(self)
Cross-Origin Headers
Cross-Origin-Opener-Policy (COOP)
Cross-Origin-Opener-Policy: same-origin
Prevents other sites from gaining a reference to your window object. Required for SharedArrayBuffer and high-resolution timers.
Cross-Origin-Resource-Policy (CORP)
Cross-Origin-Resource-Policy: same-origin
Prevents other sites from loading your resources (images, scripts) — defense against Spectre-like attacks.
Platform-Specific Configuration
Next.js
// next.config.ts
const nextConfig = {
async headers() {
return [{
source: '/:path*',
headers: [
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
{ key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
{ key: 'Strict-Transport-Security', value: 'max-age=31536000; includeSubDomains; preload' },
{ key: 'Content-Security-Policy', value: "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; frame-ancestors 'none'" },
{ key: 'Cross-Origin-Opener-Policy', value: 'same-origin' },
],
}];
},
};
For more Next.js hardening techniques beyond headers, see our Next.js security best practices guide.
Vercel (vercel.json)
If you're deploying to Vercel, you can also set headers in vercel.json. This works for any framework, not just Next.js:
{
"headers": [
{
"source": "/(.*)",
"headers": [
{ "key": "X-Frame-Options", "value": "DENY" },
{ "key": "X-Content-Type-Options", "value": "nosniff" },
{ "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" },
{ "key": "Strict-Transport-Security", "value": "max-age=31536000; includeSubDomains; preload" },
{ "key": "Permissions-Policy", "value": "camera=(), microphone=(), geolocation=()" },
{
"key": "Content-Security-Policy",
"value": "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; frame-ancestors 'none'"
},
{ "key": "Cross-Origin-Opener-Policy", "value": "same-origin" }
]
}
]
}
For a complete Vercel deployment hardening walkthrough, check our Vercel deployment security checklist.
Netlify (netlify.toml or _headers)
Option 1: netlify.toml
[[headers]]
for = "/*"
[headers.values]
X-Frame-Options = "DENY"
X-Content-Type-Options = "nosniff"
Referrer-Policy = "strict-origin-when-cross-origin"
Strict-Transport-Security = "max-age=31536000; includeSubDomains; preload"
Permissions-Policy = "camera=(), microphone=(), geolocation=()"
Content-Security-Policy = "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; frame-ancestors 'none'"
Cross-Origin-Opener-Policy = "same-origin"
Option 2: _headers file (place in your publish directory)
/*
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Permissions-Policy: camera=(), microphone=(), geolocation=()
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; frame-ancestors 'none'
Cross-Origin-Opener-Policy: same-origin
Express.js with helmet.js
helmet is the standard middleware for setting security headers in Express/Node.js applications:
npm install helmet
import express from 'express';
import helmet from 'helmet';
const app = express();
// Default helmet enables most security headers
app.use(helmet());
// For fine-grained control:
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
fontSrc: ["'self'"],
connectSrc: ["'self'", "https://api.yourdomain.com"],
frameAncestors: ["'none'"],
objectSrc: ["'none'"],
upgradeInsecureRequests: [],
},
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true,
},
frameguard: { action: 'deny' },
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
})
);
Helmet enables X-Content-Type-Options: nosniff, X-Frame-Options, HSTS, and several other headers by default. You only need to configure contentSecurityPolicy explicitly because its directives are application-specific.
Apache (.htaccess)
<IfModule mod_headers.c>
Header set X-Frame-Options "DENY"
Header set X-Content-Type-Options "nosniff"
Header set Referrer-Policy "strict-origin-when-cross-origin"
Header set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
Header set Permissions-Policy "camera=(), microphone=(), geolocation=()"
Header set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; frame-ancestors 'none'"
Header set Cross-Origin-Opener-Policy "same-origin"
</IfModule>
Nginx
server {
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; frame-ancestors 'none'" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
}
Important: The always parameter in Nginx ensures headers are sent on error responses too (404, 500, etc.), not just 200 OK.
Testing Your Headers
Using curl
The fastest way to check your security headers from the command line:
# Check all response headers
curl -I https://yourdomain.com
# Check a specific header
curl -sI https://yourdomain.com | grep -i content-security-policy
# Check headers on a specific route (API routes often differ)
curl -I https://yourdomain.com/api/health
# Follow redirects (important if you have HTTP → HTTPS redirects)
curl -IL http://yourdomain.com
What to look for:
HTTP/2 200
content-security-policy: default-src 'self'; script-src 'self' ...
strict-transport-security: max-age=31536000; includeSubDomains; preload
x-frame-options: DENY
x-content-type-options: nosniff
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
If any of these are missing, the corresponding protection is not active.
Using Browser DevTools
- Open your site in Chrome or Firefox
- Open DevTools (F12)
- Go to the Network tab
- Reload the page
- Click on the main document request
- Look at the Response Headers section
This shows you exactly what headers the browser received. It's especially useful for verifying CSP because the browser console will also show CSP violation errors if your policy is too strict.
Checking Different Routes
Security headers can vary between routes. A common mistake is setting headers only on your main pages but forgetting API routes, static assets, or error pages. Test multiple endpoints:
# Homepage
curl -sI https://yourdomain.com | grep -i "content-security\|strict-transport\|x-frame\|x-content-type"
# API route
curl -sI https://yourdomain.com/api/health | grep -i "content-security\|strict-transport\|x-frame\|x-content-type"
# Static asset
curl -sI https://yourdomain.com/favicon.ico | grep -i "content-security\|strict-transport\|x-frame\|x-content-type"
# 404 page
curl -sI https://yourdomain.com/nonexistent-page | grep -i "content-security\|strict-transport\|x-frame\|x-content-type"
Automated Scanning
Manual checks work for spot-checking, but automated scanning catches issues across your entire site — including inconsistencies between routes, weak configurations, and missing headers you might not think to test.
CheckVibe's Security Headers Scanner checks all pages discovered by site crawling for:
- Missing headers (CSP, HSTS, X-Frame-Options, etc.)
- Weak configurations (short HSTS max-age, permissive CSP)
- Misconfigurations (
unsafe-eval, wildcard sources) - Inconsistent headers across different routes
Check your security headers now — free scan, 60 seconds, 100+ total checks.
Related Reading
- CORS Misconfiguration Guide — Understand how CORS headers interact with security headers and how misconfigurations expose your API
- How to Find XSS Vulnerabilities — CSP blocks XSS at the browser level, but finding and fixing the underlying injection points is equally important
- Next.js Security Best Practices — Security headers are just one layer; see the full picture for Next.js hardening
- Vercel Deployment Security Checklist — Platform-specific security settings for Vercel-hosted applications
FAQ
Which security headers are most important?
Content-Security-Policy (CSP) and Strict-Transport-Security (HSTS) are the two most impactful headers. CSP prevents XSS — the most common web vulnerability — by controlling which scripts the browser can execute. HSTS prevents protocol downgrade attacks by forcing HTTPS connections. Together, they address the two largest categories of header-related attacks. After those, prioritize X-Content-Type-Options and X-Frame-Options, which are trivial to add and have no risk of breaking your site.
Will security headers break my site?
Most security headers are safe to deploy immediately. X-Content-Type-Options, X-Frame-Options, Referrer-Policy, and Permissions-Policy almost never cause issues. The one header that can break things is Content-Security-Policy, because it restricts which resources your page can load. If your CSP blocks a script or stylesheet your site needs, that resource will fail to load. This is why CSP has a Content-Security-Policy-Report-Only mode — deploy in report-only first, monitor for a week or two, fix any violations, then switch to enforced mode. HSTS with preload is also semi-permanent (hard to undo once submitted to browser preload lists), so make sure all subdomains support HTTPS before enabling it.
How do I test my security headers?
Three approaches, from quickest to most thorough: (1) curl — run curl -I https://yourdomain.com to see all response headers instantly. (2) Browser DevTools — open the Network tab, reload the page, and inspect the Response Headers on the document request. This also shows CSP violations in the Console tab. (3) Automated scanning — tools like CheckVibe scan every page on your site and flag missing, weak, or inconsistent headers across all routes. This catches edge cases like API routes or error pages that have different headers than your main pages.
What's the difference between CSP and X-Frame-Options?
They overlap in one specific area: clickjacking protection. X-Frame-Options: DENY prevents your page from being embedded in iframes. CSP's frame-ancestors 'none' directive does the same thing. The difference is that CSP frame-ancestors is the modern standard (more flexible — you can allow specific origins), while X-Frame-Options is the older approach with broader browser support. Best practice: set both. Use frame-ancestors 'none' in your CSP and X-Frame-Options: DENY as a fallback for older browsers. Beyond clickjacking, CSP does far more — it controls script execution, style loading, image sources, API connections, and more. X-Frame-Options only addresses framing.