webmasterhelper

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

HeaderRecommended valueWhat it does
Strict-Transport-Securitymax-age=31536000; includeSubDomainsForces HTTPS for the whole domain for a year, closing the gap on the first request that a plain redirect cannot protect.
X-Content-Type-OptionsnosniffStops the browser second-guessing your Content-Type. No downside; set it everywhere.
Referrer-Policystrict-origin-when-cross-originSends the full URL within your own site, origin only to third parties, nothing when downgrading to HTTP.
X-Frame-OptionsSAMEORIGINBlocks framing by other sites. Superseded by CSP frame-ancestors, but still honoured and still worth sending.
Permissions-Policygeolocation=(), 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'
DirectivePurpose
default-srcFallback for every fetch directive you do not set explicitly. Start at 'self'.
script-srcWhere scripts may come from. The directive that matters most.
style-srcStylesheets. Inline styles need 'unsafe-inline' or a hash, which is why style attributes are worth removing.
img-srcImages. Usually needs data: for inlined SVG and https: if you embed remote images.
connect-srcfetch, XHR, WebSocket and EventSource destinations.
frame-ancestorsWho may frame you. Replaces X-Frame-Options and is not covered by default-src.
base-uriRestricts <base>, which can otherwise rewrite every relative URL on the page.
object-srcSet to 'none' unless you genuinely serve Flash or applets.
upgrade-insecure-requestsRewrites 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.

HeaderValueEffect
Cross-Origin-Opener-Policysame-originSevers the window reference a page you open keeps to yours.
Cross-Origin-Resource-Policysame-originStops other sites embedding your resources as a side channel.
Cross-Origin-Embedder-Policyrequire-corpRequires every subresource to opt in. Breaks third-party embeds, so test carefully.

Retired — remove these if you still send them

HeaderWhy it is gone
X-XSS-ProtectionThe XSS auditor it controlled was removed from Chrome and never existed in Firefox. It introduced its own vulnerabilities. CSP replaces it.
Public-Key-PinsHPKP was deprecated and removed because a mistake bricked your domain for the pin duration with no recovery. Certificate Transparency replaced it.
Expect-CTObsolete; CT enforcement is now unconditional in major browsers.
Feature-PolicyRenamed 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;