devkult_
tools29converters34
home/blog/security-headers-checklist
securityhttpheaders

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:

  1. max-age=300 — five minutes, to test.
  2. max-age=86400; includeSubDomains — once you've confirmed every subdomain serves HTTPS.
  3. max-age=31536000; includeSubDomains — a year, the usual production value.
  4. Add preload and 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 0 or 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.

More from the blog