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.
| 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 |
| 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) |
| 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 |
| 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 |
| 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 |
| 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 |
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.
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.
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.
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.
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.
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.
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.preload without meeting the rules: the preload list needs max-age of at least 31536000 and includeSubDomains.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.ALLOW-FROM: modern browsers ignore the whole header when they see it, leaving the page frameable. Use frame-ancestors with the origin instead.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.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.curl -I.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.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.Content-Security-Policy value for you, directive by directiveadd_header