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.
Originally published at devkult.com. To build all six for your server or host in one go, use the free Security Headers Generator.
Top comments (0)