Security · 9 minute read
Security headers: what to set, in what order
Nine headers, most of them one line, and between them they close off the majority of client-side attacks. Add them in this order: the cheap ones first, CSP last, because CSP is the one that breaks things.
Set these today
| Header | Recommended value | What it does |
|---|---|---|
Strict-Transport-Security | max-age=31536000; includeSubDomains | Forces HTTPS for the whole domain for a year, closing the gap on the first request that a plain redirect cannot protect. |
X-Content-Type-Options | nosniff | Stops the browser second-guessing your Content-Type. No downside; set it everywhere. |
Referrer-Policy | strict-origin-when-cross-origin | Sends the full URL within your own site, origin only to third parties, nothing when downgrading to HTTP. |
X-Frame-Options | SAMEORIGIN | Blocks framing by other sites. Superseded by CSP frame-ancestors, but still honoured and still worth sending. |
Permissions-Policy | geolocation=(), camera=(), microphone=() | Switches off browser features the site does not use, including for embedded third-party frames. |
Content-Security-Policy
The most effective header and the most work. Start in report-only mode so nothing breaks while you find out what the page really loads:
add_header Content-Security-Policy-Report-Only
"default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; report-uri /csp-report" always;
Collect reports for a week or two, then enforce. A policy with 'unsafe-inline' in script-src is mostly decoration — it permits exactly the injection CSP exists to stop. Use a per-request nonce instead:
<script nonce="r4nd0mPerRequest">/* inline code */</script>
# and in the policy
script-src 'self' 'nonce-r4nd0mPerRequest'
| Directive | Purpose |
|---|---|
default-src | Fallback for every fetch directive you do not set explicitly. Start at 'self'. |
script-src | Where scripts may come from. The directive that matters most. |
style-src | Stylesheets. Inline styles need 'unsafe-inline' or a hash, which is why style attributes are worth removing. |
img-src | Images. Usually needs data: for inlined SVG and https: if you embed remote images. |
connect-src | fetch, XHR, WebSocket and EventSource destinations. |
frame-ancestors | Who may frame you. Replaces X-Frame-Options and is not covered by default-src. |
base-uri | Restricts <base>, which can otherwise rewrite every relative URL on the page. |
object-src | Set to 'none' unless you genuinely serve Flash or applets. |
upgrade-insecure-requests | Rewrites http:// subresources to https:// — a useful backstop on a large legacy site. |
Cross-origin isolation
Needed if you use SharedArrayBuffer or high-resolution timers, and useful defence in depth otherwise.
| Header | Value | Effect |
|---|---|---|
Cross-Origin-Opener-Policy | same-origin | Severs the window reference a page you open keeps to yours. |
Cross-Origin-Resource-Policy | same-origin | Stops other sites embedding your resources as a side channel. |
Cross-Origin-Embedder-Policy | require-corp | Requires every subresource to opt in. Breaks third-party embeds, so test carefully. |
Retired — remove these if you still send them
| Header | Why it is gone |
|---|---|
X-XSS-Protection | The XSS auditor it controlled was removed from Chrome and never existed in Firefox. It introduced its own vulnerabilities. CSP replaces it. |
Public-Key-Pins | HPKP was deprecated and removed because a mistake bricked your domain for the pin duration with no recovery. Certificate Transparency replaced it. |
Expect-CT | Obsolete; CT enforcement is now unconditional in major browsers. |
Feature-Policy | Renamed to Permissions-Policy with different syntax. Sending the old name alone does nothing in current browsers. |
A complete nginx block
# Inside the server {} block. "always" makes nginx send them on error
# responses too, which is where people most often lose them.
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), camera=(), microphone=()" always;
add_header Content-Security-Policy "default-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'self'" always;