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.
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.
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:
frame-ancestors 'none'connect-srcCommon 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.
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 |
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:
Content-Security-Policy-Report-Onlyreport-uri endpoint to collect violation reportsReport-Only to enforced Content-Security-PolicyThis approach lets you tighten security iteratively without breaking your site in production.
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.
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 accessImportant: 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.
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.
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.
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).
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-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: same-origin
Prevents other sites from loading your resources (images, scripts) — defense against Spectre-like attacks.
// 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.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.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
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.
.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>
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.
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.
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.
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"
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:
unsafe-eval, wildcard sources)Check your security headers now — free scan, 60 seconds, 100+ total checks.
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.
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.
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.
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.
Paste your URL and get a security report in 30 seconds. 100+ automated checks with AI-powered fix prompts.
Scan your site freeRelated articles
Learn what website security scanning is, how it works, the different types of scans, and why every developer should automate it. Beginner-friendly guide.
Learn how automated security scanners find vulnerabilities in your website before attackers do. Covers SQL injection, XSS, exposed API keys, and 27 more checks.
How CSRF attacks work and how to prevent them. Covers CSRF tokens, SameSite cookies, custom headers, and framework-specific protection for Next.js, Express, and Django.