Security Headers Checklist: 6 HTTP Headers Every Site Should Send
Security headers are the cheapest hardening you'll ever ship: a few lines of server config, no code changes, and whole classes of attack — clickjacking, MIME sniffing, protocol downgrades, much of XSS — get harder or impossible. Here are the six worth sending on every site, in the order that's safest to roll them out.
The checklist
| Header | Recommended value | Stops |
|---|---|---|
| X-Content-Type-Options | nosniff |
Files being run as a type they weren't served as |
| Referrer-Policy | strict-origin-when-cross-origin |
Full URLs leaking to other sites |
| X-Frame-Options | DENY (or CSP frame-ancestors 'none') |
Clickjacking via iframes |
| Permissions-Policy | camera=(), microphone=(), geolocation=() |
Injected code asking for powerful features |
| Strict-Transport-Security | max-age=31536000; includeSubDomains |
HTTPS → HTTP downgrades |
| Content-Security-Policy | depends on your site — start in report-only mode | Most XSS |
The first four are nearly risk-free: they rarely break anything. HSTS and CSP need care, so they come last. To build all six at once for your server or host, use the security headers generator.
1–4: the safe ones
X-Content-Type-Options: nosniff makes browsers trust your Content-Type instead of guessing. The only thing it can break is a script or stylesheet served with the wrong type, which you want to fix anyway.
Referrer-Policy: strict-origin-when-cross-origin is already the default in current browsers; sending it makes the behaviour explicit for older ones. Use no-referrer or same-origin if your URLs carry anything sensitive.
X-Frame-Options: DENY stops other sites from loading your pages in an iframe. Use SAMEORIGIN if you frame your own pages. Once you have a CSP, its frame-ancestors directive does the same job and is the modern replacement — send both for older browsers.
Permissions-Policy switches off browser features you don't use. List the ones that matter — camera, microphone, geolocation, payment — with an empty allowlist ().
5: HSTS, carefully
Strict-Transport-Security tells browsers to use HTTPS for your domain, every time, for max-age seconds — even if someone types http://. It's powerful precisely because it can't be undone quickly: if a subdomain doesn't support HTTPS and you've sent includeSubDomains, browsers will refuse to load it until the max-age runs out.
Roll it out in steps:
max-age=300— five minutes, to test.max-age=86400; includeSubDomains— once you've confirmed every subdomain serves HTTPS.max-age=31536000; includeSubDomains— a year, the usual production value.- Add
preloadand submit the domain to hstspreload.org only if you're sure; removal takes months.
6: CSP, in report-only mode first
A Content-Security-Policy tells the browser which sources may provide scripts, styles, images and frames. A good one turns most injected-script XSS into a blocked request. It's also the header most likely to break your site, so start with Content-Security-Policy-Report-Only: the browser reports violations to the console (and to an endpoint, if you configure one) without blocking anything.
A starting policy for a site that serves its own assets:
Content-Security-Policy: default-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'
Third-party scripts, analytics and inline <script> blocks all need explicit allowances — the CSP guide covers nonces, hashes and the usual breakages.
Setting them
Next.js (next.config.js):
const securityHeaders = [
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
{ key: "X-Frame-Options", value: "DENY" },
{ key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" },
{ key: "Strict-Transport-Security", value: "max-age=31536000; includeSubDomains" },
];
module.exports = {
async headers() {
return [{ source: "/(.*)", headers: securityHeaders }];
},
};
nginx (inside the HTTPS server block — always adds them to error pages too):
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header X-Frame-Options "DENY" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
One nginx trap: add_header directives are inherited only if the inner block defines none of its own. A single add_header inside a location silently drops every header set at the server level for that location.
Express: the Helmet middleware sets sensible defaults for all of these, including a starter CSP — app.use(helmet()).
Vercel (vercel.json) and Netlify (_headers):
{
"headers": [
{
"source": "/(.*)",
"headers": [{ "key": "X-Content-Type-Options", "value": "nosniff" }]
}
]
}
/*
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Headers you can drop
- X-XSS-Protection — the browser XSS filters it controlled have been removed. Send
0or nothing. - Public-Key-Pins (HPKP) — removed from browsers; it could lock users out of a site permanently.
- Expect-CT — obsolete now that Certificate Transparency is enforced by default.
- Feature-Policy — replaced by Permissions-Policy.
Check your work
curl -sI https://your-site.example | grep -iE "strict-transport|content-security|x-content-type|referrer-policy|permissions-policy|x-frame"
To check many URLs at once, paste them into the HTTP Status Checker. For every header's full set of values, see the HTTP headers reference.