Security response headers tell the browser what a page may load, whether another site may frame it, and whether it must only be reached over HTTPS. They cost a few lines of server configuration. The reference used throughout this article is the OWASP HTTP Security Response Headers Cheat Sheet, with Google's guide to a strict Content Security Policy for CSP.
The headers to set
| Header | Value recommended by OWASP | What it does |
|---|---|---|
Strict-Transport-Security |
max-age=63072000; includeSubDomains; preload |
The browser only uses HTTPS for the site. See the rollout notes below before copying this value. |
Content-Security-Policy |
a strict, nonce-based policy (see below) | Limits which scripts and resources can run or load. |
X-Content-Type-Options |
nosniff |
Stops the browser from guessing a MIME type different from Content-Type. |
X-Frame-Options |
DENY |
Blocks framing (clickjacking). CSP frame-ancestors supersedes it in browsers that support it. |
Referrer-Policy |
strict-origin-when-cross-origin |
Sends only the origin to other sites. It is the modern browser default; OWASP recommends setting it explicitly. |
Permissions-Policy |
geolocation=(), camera=(), microphone=() |
Disables browser features the site does not use, even if an XSS tries to turn them on. |
Cross-Origin-Opener-Policy |
same-origin |
Keeps cross-origin documents out of the page's browsing context group. |
Cache-Control |
no-store on sensitive responses |
Prevents storage in compliant HTTP caches. no-cache still allows storage, with revalidation. |
Set Content-Type with a charset on HTML (text/html; charset=UTF-8). OWASP notes the charset is needed to prevent XSS in HTML pages.
Headers to remove
X-XSS-Protection: OWASP recommends not setting it, or settingX-XSS-Protection: 0. In some cases the filter it enables creates XSS vulnerabilities in sites that were otherwise safe.Expect-CT: do not use it.Public-Key-Pins: removed from Chromium in 2018 and unsupported by modern browsers. Remove it from production.ServerandX-Powered-By: remove them, or set a value that names no software or version.
Content Security Policy: strict, then enforced
The first CSPs were allowlists of domains. The OWASP CSP cheat sheet now recommends a strict CSP, based on a nonce or a hash, which it describes as easier to deploy and less likely to be bypassed. The nonce version, as given in Google's guide:
Content-Security-Policy:
script-src 'nonce-{RANDOM}' 'strict-dynamic';
object-src 'none';
base-uri 'none';
The nonce is a new random value for every response. The application, not the web server, must generate it, because the same value has to appear in the header and on each <script nonce="..."> tag of the page. strict-dynamic lets scripts loaded by a trusted script run, which keeps most third-party libraries working. object-src 'none' disables plugins, and base-uri 'none' blocks injected <base> tags.
Add frame-ancestors 'none' (or the list of origins allowed to frame you) to the same header to replace X-Frame-Options.
To roll it out without breaking pages, send the policy first as Content-Security-Policy-Report-Only. The browser reports what it would have blocked and blocks nothing. OWASP notes that both headers can be sent together, for example a strict report-only policy next to a looser enforced one. Switch to the enforcing header once the reports are clean.
Two limits from the OWASP CSP cheat sheet: a CSP delivered in a <meta> tag cannot use framing protection, sandboxing or a reporting endpoint, and CSP is a second layer. It does not replace output encoding and the rest of the XSS prevention work.
HSTS without locking users out
HSTS tells browsers to refuse plain HTTP for your domain. A mistake lasts as long as max-age. The OWASP HSTS cheat sheet gives example values that, taken in order, make a safe rollout:
- Start with a short duration during the rollout:
max-age=86400; includeSubDomains. - Add
includeSubDomainsonly if every present and future subdomain is served over HTTPS. - Move to
max-age=63072000(two years) once the configuration is stable. - Add
preloadonly after reading the preload removal rules. OWASP warns it can have permanent consequences. The flag only gives consent; the domain still has to be submitted to the preload list.
The main HTTP headers cheat sheet adds a warning: with a long max-age, an expired or revoked certificate can leave legitimate users unable to reach the site until the duration runs out.
Two nginx traps
The nginx add_header directive has two behaviors that silently drop headers.
Status codes. Without the always parameter, nginx adds the header only to responses with codes 200, 201, 204, 206, 301, 302, 303, 304, 307 and 308. Your 404 and 500 pages go out without them.
Inheritance. add_header directives are inherited from the previous level only if the current level defines none. One add_header inside a location block removes every header set at the server level for that location. Since nginx 1.29.3, add_header_inherit merge; changes this behavior; on older versions, repeat the headers or keep them in a file you include in each block.
A server-level block that avoids both traps:
add_header Strict-Transport-Security "max-age=86400; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), camera=(), microphone=()" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
The CSP stays in the application, since it carries the per-response nonce.
Cookies
Set-Cookie is not a security header, but its attributes are. The OWASP Session Management Cheat Sheet calls Secure and HttpOnly mandatory for session cookies, and requires an explicit SameSite=Strict (preferred) or SameSite=Lax. It treats SameSite as defense in depth against CSRF, not as a replacement for a CSRF token.
Set-Cookie: session=...; Secure; HttpOnly; SameSite=Strict; Path=/
Check what the server sends
Check a normal page, an error page and an API response:
curl -sI https://example.com/
curl -sI https://example.com/this-page-does-not-exist
curl -sI https://example.com/api/health
If the headers appear on the first response and not on the 404, an always is missing. For JSON APIs, OWASP notes that CSP and X-Frame-Options bring nothing on a response the browser does not render; nosniff and correct Content-Type still apply.
Official sources
- OWASP HTTP Security Response Headers Cheat Sheetcheatsheetseries.owasp.org
- OWASP Content Security Policy Cheat Sheetcheatsheetseries.owasp.org
- web.dev (Google): Mitigate XSS with a strict Content Security Policyweb.dev
- OWASP HTTP Strict Transport Security Cheat Sheetcheatsheetseries.owasp.org
- OWASP Session Management Cheat Sheetcheatsheetseries.owasp.org
- nginx documentation: ngx_http_headers_modulenginx.org