Cheatsheet

Security Headers Cheatsheet

This cheatsheet lists the HTTP response headers that switch on browser-side protections, with a safe starting value for each and what happens when the header is missing. It is for developers hardening a site who want one table of values rather than seven separate documentation pages. The one confusion it clears up is that most of these headers do nothing unless the browser receives them in an HTTP response; several are ignored in a <meta> tag or over plain HTTP. Related terms are XSS, HTTPS and TLS.

Quick reference

Recommended starting values

Header Starting value Protects against
`Strict-Transport-Security` `max-age=31536000; includeSubDomains` Downgrade to HTTP, cookie theft on first plain request
`Content-Security-Policy` `default-src 'self'; frame-ancestors 'none'; base-uri 'none'` Injected scripts, clickjacking, base tag hijack
`X-Content-Type-Options` `nosniff` Scripts and styles loaded with the wrong MIME type
`Referrer-Policy` `strict-origin-when-cross-origin` Leaking full URLs to other sites
`Permissions-Policy` `camera=(), microphone=(), geolocation=()` Unwanted use of powerful browser features
`Cross-Origin-Opener-Policy` `same-origin` Cross-window access through `window.opener`
`X-Frame-Options` `DENY` or `SAMEORIGIN` Clickjacking in older browsers

Strict-Transport-Security (HSTS)

Part Detail
`max-age` Required. Seconds the browser keeps forcing HTTPS. `31536000` is one year; `0` clears the entry
`includeSubDomains` Optional. Applies the rule to every subdomain
`preload` Optional and non-standard. Needs `max-age` of at least `31536000` plus `includeSubDomains`
Over HTTP Must not be sent, and browsers ignore it if it arrives over insecure transport (RFC 6797)

Referrer-Policy values

Value Cross-origin HTTPS to HTTPS HTTPS to HTTP Same-origin
`no-referrer` nothing nothing nothing
`origin` origin only origin only origin only
`same-origin` nothing nothing full URL
`strict-origin` origin only nothing origin only
`strict-origin-when-cross-origin` (default) origin only nothing full URL
`no-referrer-when-downgrade` full URL nothing full URL
`unsafe-url` full URL full URL full URL

Framing and isolation

Header Values Notes
`X-Frame-Options` `DENY`, `SAMEORIGIN` `ALLOW-FROM` is obsolete and ignored by modern browsers
`frame-ancestors` (CSP) `'none'`, `'self'`, origins Replaces `X-Frame-Options`; ignored inside a `<meta>` tag
`Cross-Origin-Opener-Policy` `unsafe-none` (default), `same-origin-allow-popups`, `same-origin` `same-origin` isolates the browsing context group

Permissions-Policy syntax

Value Meaning
`geolocation=()` Disabled everywhere, including iframes
`camera=(self)` Allowed for the page's own origin only
`geolocation=(self "https://maps.example")` Own origin plus one listed origin, which must be quoted
`fullscreen=*` Allowed for all origins

Header or meta tag

Policy Works as `<meta http-equiv>`?
`Content-Security-Policy` Yes, with fewer features; `frame-ancestors` is ignored
`Content-Security-Policy-Report-Only` No, header only
`X-Frame-Options` No effect
`Strict-Transport-Security` No; RFC 6797 defines it only as an HTTP response header field

Common patterns

Send all the headers from one Node server

const http = require("http");
const headers = {
  "Strict-Transport-Security": "max-age=31536000; includeSubDomains",
  "Content-Security-Policy": "default-src 'self'; frame-ancestors 'none'; base-uri 'none'",
  "X-Content-Type-Options": "nosniff",
  "Referrer-Policy": "strict-origin-when-cross-origin",
  "Permissions-Policy": "camera=(), microphone=(), geolocation=()",
  "Cross-Origin-Opener-Policy": "same-origin",
};
http.createServer((req, res) => {
  for (const [k, v] of Object.entries(headers)) res.setHeader(k, v);
  res.end("ok");
}).listen(8090, async () => {
  const r = await fetch("http://localhost:8090/");
  for (const [k, v] of r.headers) console.log(k + ": " + v);
  process.exit();
});

Output, with Node v22.22.0:

connection: keep-alive
content-length: 2
content-security-policy: default-src 'self'; frame-ancestors 'none'; base-uri 'none'
cross-origin-opener-policy: same-origin
date: Mon, 05 Oct 2026 09:36:15 GMT
keep-alive: timeout=5
permissions-policy: camera=(), microphone=(), geolocation=()
referrer-policy: strict-origin-when-cross-origin
strict-transport-security: max-age=31536000; includeSubDomains
x-content-type-options: nosniff

HSTS only takes effect when this is served over HTTPS; this local test used plain HTTP, so it shows the header text but no browser would act on it.

Check what a framed page does

none            -> framed-content
xfo-deny        -> Refused to display 'http://localhost:8082/' in a frame because it set 'X-Frame-Options' to 'deny'.
fa-self         -> Framing 'http://localhost:8082/' violates the following Content Security Policy directive: "frame-ancestors 'self'". The request has been blocked.
fa-origin       -> framed-content   (frame-ancestors listed the parent's origin)
meta            -> framed-content   (The Content Security Policy directive 'frame-ancestors' is ignored when delivered via a <meta> element.)

This ran in Chromium 153 with a parent page on 127.0.0.1:8083 embedding a child on localhost:8082. Different ports count as different origins, so fa-self blocked the frame.

Block a script served with the wrong MIME type

X-Content-Type-Options: nosniff
Content-Type: text/plain      ->  Refused to execute script ... because its MIME type ('text/plain') is not executable, and strict MIME type checking is enabled.

Without the header, the same text/plain script ran. nosniff blocks script requests unless the type is a JavaScript MIME type, and style requests unless it is text/css.

Confirm the default Referrer-Policy

no header:                      Referer: http://localhost:8082/
Referrer-Policy: no-referrer:   Referer: (none)

Both lines are what a cross-origin link click sent from a page at http://localhost:8082/ref. With no header, Chromium 153 sent the origin only, which matches the strict-origin-when-cross-origin default.

Trial a policy without breaking the site

Content-Security-Policy-Report-Only: script-src 'self'
page with an inline script  ->  ran: ["inline"]
console: Executing inline script violates the following Content Security Policy directive 'script-src 'self''.

In report-only mode the inline script still ran in Chromium 153, but the violation was logged. Ship the report-only header first, read the violations, then rename it to Content-Security-Policy. Report-only cannot be delivered in a meta tag.

Turn off a feature with Permissions-Policy

Permissions-Policy: geolocation=(), camera=(self)
no header:  geolocation:true camera:true microphone:true
with header: geolocation:false camera:true microphone:true

The values are document.featurePolicy.allowsFeature(name) results in Chromium 153. Unlisted features stay at their defaults.

Pitfalls

  • Setting HSTS on a site with an HTTP-only subdomain: includeSubDomains forces every subdomain to HTTPS, and browsers then refuse the HTTP-only host. Audit subdomains before adding it, and start with a short max-age.
  • Sending preload without meeting the rules: the preload list needs max-age of at least 31536000 and includeSubDomains.
  • Putting X-Frame-Options in a meta tag: it has no effect there. Send it as a real header, or use CSP frame-ancestors in the header; frame-ancestors in a meta tag is ignored too.
  • Using ALLOW-FROM: modern browsers ignore the whole header when they see it, leaving the page frameable. Use frame-ancestors with the origin instead.
  • Relying on X-XSS-Protection: it is deprecated and non-standard, and a strict CSP that blocks inline scripts replaces it. Leave it out and rely on CSP.
  • Using nosniff with sloppy MIME types: once enabled, scripts served as text/plain or text/html stop running. Fix the Content-Type on the server; do not remove the header.
  • Testing HSTS over plain HTTP: a browser ignores the header when it arrives over insecure transport, so a local HTTP test proves nothing. Check it on a real HTTPS response with curl -I.
  • Setting Cross-Origin-Opener-Policy: same-origin without testing popups: a document with same-origin shares a browsing context group only with same-origin documents that also send same-origin. A cross-origin popup, such as a payment or login window, loses its link to the opener. Try same-origin-allow-popups when you need to keep opened windows.
  • Setting a header only on the main page: headers apply per response. Static assets, error pages and API routes behind a different proxy rule may miss them. Check with a request to each.
  • Expecting a Permissions-Policy value to grant access: the test above shows camera=(self) only reports the feature as allowed to the page. Browser permission prompts are a separate layer, so the header cannot turn a feature on by itself.

Related ZipKit tools

Related cheatsheets