Security headers

Close whole categories of attack with a handful of response headers.

11of 18
≈ 12Min read
5Parts

Strict-Transport-Security

Without it, a visitor typing doors.test makes one plain-http request that can be intercepted before your redirect ever arrives. With it, the browser rewrites http to https itself for the next year — and the redirect round trip disappears too.

includeSubDomains covers everything under the name, so check that every subdomain has a certificate first. Start with a short max-age, confirm nothing broke, then raise it.

https, from now on, without asking
Strict-Transport-Security: max-age=31536000; includeSubDomains
It is hard to undo

A browser that has seen the header honours it until it expires, and there is no way to tell it otherwise. A year of https with a certificate you forgot to renew on a subdomain is a year of an unreachable site.

Content-Security-Policy

CSP is the second line of defence against cross-site scripting: even if an attacker injects a <script>, the browser refuses to run it because it is not from an allowed source and carries no nonce.

It is only worth having if it is strict. script-src 'self' 'unsafe-inline' allows exactly the injected inline script you were trying to stop — and unsafe-inline is what most first attempts end up with, because the page has inline handlers. Removing them is the work; the header is the easy part.

what this page is allowed to load and run
Content-Security-Policy:
  default-src 'self';
  script-src  'self' 'nonce-r4nd0m';
  style-src   'self';
  img-src     'self' data: https://cdn.doors.test;
  connect-src 'self' https://api.doors.test;
  frame-ancestors 'none';
  base-uri    'self';
  form-action 'self'
Report first

Content-Security-Policy-Report-Only with a report-to endpoint tells you what would have been blocked, on real traffic, without breaking anything. Run it for a week before enforcing.

The short ones

frame-ancestors in CSP supersedes X-Frame-Options, but sending both costs nothing and covers older browsers.

HeaderStops
X-Content-Type-Options: nosniffthe browser guessing that your JSON is HTML and running it
Referrer-Policy: strict-origin-when-cross-originleaking a full URL — with its query — to other sites
X-Frame-Options: DENY / frame-ancestors 'none'your page being framed invisibly over a button (clickjacking)
Permissions-Policy: camera=(), geolocation=()embedded content asking for hardware in your name
Cross-Origin-Opener-Policy: same-originanother window keeping a handle on yours

Where the defaults are wrong

  • Serving user uploads from your own origin. A file called avatar.html is then a script on your domain, with your cookies. Serve uploads from a separate origin, with nosniff and Content-Disposition: attachment.
  • Referrer-Policy: unsafe-url — the whole URL, including a password-reset token in the query, goes to every site the user clicks through to.
  • A CSP with unsafe-eval because one library needs it. That library is running eval on something; find out what.

Headers are not the whole answer

Escaping output prevents XSS; CSP limits what a missed escape can do. Validating input prevents injection; a prepared statement makes it impossible. Headers are the layer that turns a mistake into a blocked console message instead of an incident — they are worth an afternoon, and they are not a substitute for the other layers.

Practice