Guide
HTTP Security Headers Checklist: Values and Configs
Which HTTP security headers to send and which to drop: starting values for CSP, HSTS and more, with nginx, Apache, Express, Next.js and Cloudflare config.
On this page
TL;DR: Send
Strict-Transport-Security,X-Content-Type-Options: nosniff,Referrer-Policy: strict-origin-when-cross-origin, a frame-protection header and a Content-Security-Policy you ship as Report-Only first; addPermissions-PolicyandCross-Origin-*headers where your site allows. Do not sendExpect-CT,Public-Key-PinsorFeature-Policy, and setX-XSS-Protectiononly to0or leave it out. Check your result with the Site Verifier.
The checklist
Starting values, not universal answers. Each links to the page that covers the header in depth.
| Header | Recommended starting value | What it prevents |
|---|---|---|
| Strict-Transport-Security | max-age=300 first, then max-age=31536000; includeSubDomains | SSL-stripping and downgrade to HTTP after the first HTTPS visit |
| Content-Security-Policy | Report-Only starter below | Injected scripts running (XSS impact), clickjacking via frame-ancestors, data exfiltration to unlisted hosts |
| X-Content-Type-Options | nosniff | Browsers guessing a script or stylesheet from a response served with the wrong Content-Type |
| Referrer-Policy | strict-origin-when-cross-origin | Full URLs (with tokens or IDs in the path and query) leaking to other origins in Referer |
| X-Frame-Options | DENY, or SAMEORIGIN if you frame yourself; pair with CSP frame-ancestors | Clickjacking: your page being framed by another site |
| Permissions-Policy | camera=(), microphone=(), geolocation=(), payment=() for features you do not use | Your page, or a third-party iframe on it, using powerful browser features |
| Cross-Origin-Opener-Policy | same-origin (test OAuth and payment popups first) | Cross-origin windows holding a reference to yours via window.opener; prerequisite for cross-origin isolation |
| Cross-Origin-Resource-Policy | same-site or same-origin on resources not meant to be embedded | Other sites embedding your images, scripts or JSON responses (Spectre-class and hotlinking exposure) |
| Cross-Origin-Embedder-Policy | require-corp, only if you need cross-origin isolation (SharedArrayBuffer) | Loading cross-origin resources that have not opted in; breaks third-party embeds, so skip it by default |
Cookies carry their own flags rather than headers of this kind: see Cookie Security for Secure, HttpOnly and SameSite.
Notes that save debugging time:
- HSTS is sticky. A browser that has seen
max-age=31536000refuses plain HTTP for your host for a year. Start atmax-age=300, confirm every subdomain you add withincludeSubDomainsserves HTTPS, then raise it. Addpreloadonly after reading the preload requirements; removal from the list takes months. - Set HSTS only over HTTPS. Browsers ignore it on HTTP responses.
X-Frame-Optionshas no multi-origin allow-list. TheALLOW-FROMvalue is obsolete; use CSPframe-ancestors https://partner.examplewhen a specific partner may frame you.- COOP
same-originbreaks popup flows that rely onwindow.openerorwindow.closed, such as some OAuth and payment pop-ups. Test the login flow before shipping. - A
Referrer-Policyofno-referreralso removesRefererfor your own analytics and CSRF checks that read it.strict-origin-when-cross-originis the browser default in current browsers, but setting it explicitly protects older clients and documents intent.
What not to send
| Header | Why to drop it |
|---|---|
X-XSS-Protection: 1; mode=block | The legacy filter could be abused to disable parts of a page or leak information, and Chrome removed its auditor. Send X-XSS-Protection: 0 to switch off any remaining legacy filter, or omit it. See X-XSS-Protection. |
Expect-CT | Existed to enforce Certificate Transparency before it became a baseline requirement for publicly trusted certificates. Browsers no longer act on it and MDN lists it as deprecated. |
Public-Key-Pins (HPKP) | Pinning the wrong key could lock users out of your site for months. Chrome removed support and no current browser honors it. |
Feature-Policy | Replaced by Permissions-Policy, which has different syntax. See Permissions-Policy vs Feature-Policy. |
Server: nginx/1.27.0, X-Powered-By: Express | Not a protection, but version strings help attackers pick exploits. Use server_tokens off; in nginx, ServerTokens Prod in Apache and remove X-Powered-By. |
Access-Control-Allow-Origin: * with credentialed responses | Browsers reject the wildcard when credentials are included, and reflecting arbitrary Origin values is worse. Use an explicit allow-list. See the CORS guide. |
A CSP starter that will not break most sites
Start with a policy that is strict about the dangerous things (plugins, <base>, form targets, framing) and tolerant about the common ones (inline styles, images from HTTPS hosts). Scripts are the part that needs your attention.
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'; report-to csp-endpoint; report-uri /csp-reports
Reporting-Endpoints: csp-endpoint="https://example.com/csp-reports"
report-to uses the endpoint named in Reporting-Endpoints. report-uri is deprecated in the spec but still the one that works in browsers that lack the Reporting API, so most deployments send both.
This starter will report, and once enforced block, inline <script> blocks, inline event handlers (onclick=), third-party script hosts and eval. That is deliberate: those are what an XSS attack uses.
Rollout plan:
- Report-Only for one full release cycle. Collect reports. Filter noise from browser extensions (
chrome-extension://,moz-extension://as theblocked-uri). - Allow-list the legitimate third parties by exact host in
script-src,img-src,connect-srcand so on. Avoid wildcards such ashttps:inscript-src. - Replace inline scripts with external files, or use per-response nonces (
script-src 'nonce-<random>' 'strict-dynamic') if you cannot remove them. A nonce must be unpredictable and different on every response, so it cannot be set statically in an nginx file. - Switch to enforcing by renaming the header to
Content-Security-Policy, keeping the same value. Keep a Report-Only copy of the next, stricter policy running next to it. - Paste the final policy into the CSP Evaluator to catch weak sources such as
'unsafe-inline'inscript-srcor wildcard hosts.
For nonces, hashes, strict-dynamic and the exact console errors, see the Content-Security-Policy page.
Config blocks
Every block below sets the same baseline. Replace the CSP with your own tested policy and keep the Report-Only value until you are ready.
nginx
server {
listen 443 ssl;
server_name example.com;
server_tokens off;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header X-Frame-Options "DENY" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'" always;
location /api/ {
# GOTCHA: this one add_header means NONE of the server-level
# headers above are sent for /api/ responses.
add_header Cache-Control "no-store" always;
proxy_pass http://app;
}
}
Because of that location /api/ block, every security header above disappears on API responses. nginx inherits add_header from the previous level only if the current level has none. The fix is to repeat them, and the clean way is a snippet:
# /etc/nginx/snippets/security-headers.conf
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header X-Frame-Options "DENY" always;
location /api/ {
include snippets/security-headers.conf;
add_header Cache-Control "no-store" always;
proxy_pass http://app;
}
Separately, without always nginx adds headers only on 200, 201, 204, 206, 301, 302, 303, 304, 307 and 308. Your 404 and 502 pages would go out unprotected. Also remember that upstream response headers pass through, so a header your app already sets can appear twice; use proxy_hide_header to drop the duplicate. Verify with curl -sI https://example.com/api/ping, not only the homepage.
Apache
Requires mod_headers.
ServerTokens Prod
ServerSignature Off
<IfModule mod_headers.c>
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set X-Frame-Options "DENY"
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
Header always set Cross-Origin-Opener-Policy "same-origin"
Header always set Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'"
Header always unset X-Powered-By
</IfModule>
Header always applies to error responses too, which plain Header set does not.
Express with Helmet
import express from 'express'
import helmet from 'helmet'
const app = express()
app.use(
helmet({
contentSecurityPolicy: {
useDefaults: true,
reportOnly: true,
directives: {
'script-src': ["'self'"],
'img-src': ["'self'", 'data:', 'https:']
}
},
crossOriginEmbedderPolicy: false
})
)
Helmet’s README lists these defaults: Content-Security-Policy (a restrictive policy including default-src 'self' and upgrade-insecure-requests), Cross-Origin-Opener-Policy: same-origin, Cross-Origin-Resource-Policy: same-origin, Origin-Agent-Cluster: ?1, Referrer-Policy: no-referrer, Strict-Transport-Security: max-age=31536000; includeSubDomains, X-Content-Type-Options: nosniff, X-DNS-Prefetch-Control: off, X-Download-Options: noopen, X-Frame-Options: SAMEORIGIN, X-Permitted-Cross-Domain-Policies: none and X-XSS-Protection: 0, and it removes X-Powered-By. Cross-Origin-Embedder-Policy is not set by default.
Two defaults commonly surprise people. Cross-Origin-Resource-Policy: same-origin blocks other origins, including your own separate frontend or CDN domain, from loading your images and API responses; override with crossOriginResourcePolicy: { policy: 'cross-origin' } where needed. And the default CSP is enforcing, which is why the example sets reportOnly: true for the first rollout. Helmet does not set Permissions-Policy; set it with res.setHeader or a small middleware.
Next.js
headers() in next.config.js covers static values. Policies that need a per-request nonce must be set in middleware instead.
// next.config.js
const securityHeaders = [
{ key: 'Strict-Transport-Security', value: 'max-age=31536000; includeSubDomains' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' }
]
module.exports = {
poweredByHeader: false,
async headers() {
return [{ source: '/(.*)', headers: securityHeaders }]
}
}
If you deploy behind another proxy that already sets these headers, you will send duplicates; set them in one place.
Cloudflare
For a site proxied through Cloudflare, add the headers at the edge without touching the origin. In the dashboard use Rules, Transform Rules, Modify Response Header, and add a “Set static” operation per header. Cloudflare also has its own HSTS toggle in the SSL/TLS settings.
For anything conditional, a Worker can set them in code:
export default {
async fetch(request) {
const response = await fetch(request)
const secured = new Response(response.body, response)
secured.headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains')
secured.headers.set('X-Content-Type-Options', 'nosniff')
secured.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin')
secured.headers.set('X-Frame-Options', 'DENY')
secured.headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()')
secured.headers.delete('X-Powered-By')
return secured
}
}
Copying the response into a new Response is required: the headers on the original returned from fetch() are immutable.
Verify
curl -sI https://example.com | grep -iE 'strict-transport|content-security|x-content-type|referrer-policy|x-frame|permissions-policy|cross-origin'
Check a 404 and an API route as well, since those are where inheritance bugs hide. For a graded report on a live URL, run the Site Verifier; for the policy itself, use the CSP Evaluator.
Frequently asked questions
What security headers should every website have?
At minimum: Strict-Transport-Security on HTTPS sites, X-Content-Type-Options: nosniff, a Referrer-Policy such as strict-origin-when-cross-origin, a frame-protection header (CSP frame-ancestors or X-Frame-Options), and a Content-Security-Policy that you roll out in Report-Only mode first. Permissions-Policy and the Cross-Origin-* headers are worth adding when you know your site does not need the features they restrict.
Why does X-XSS-Protection: 0 appear in security header recommendations?
The old XSS auditor filters in browsers could be abused to make the filter itself break or leak parts of a page, and Chrome removed its auditor. Modern guidance, including the value Helmet sends by default, is to send 0 to disable the legacy filter explicitly and rely on a Content-Security-Policy instead. Sending 1; mode=block is no longer recommended.
Why are my nginx security headers missing on some pages?
nginx add_header directives are inherited from the enclosing level only if the current level defines no add_header of its own. A single add_header inside a location block therefore discards every add_header set at server level for that location. Repeat the full set in each location, or put them in a snippet and include it everywhere, and add the always parameter so they are also sent on 4xx and 5xx responses.
Should I send Content-Security-Policy-Report-Only or enforce CSP straight away?
Start with Content-Security-Policy-Report-Only on a real site. It never blocks anything, but the browser reports every violation, so you see which inline scripts and third-party hosts your policy would have broken. Fix or allow them, run for at least a full release cycle, then switch the same value to Content-Security-Policy.
Is X-Frame-Options still needed if I have CSP frame-ancestors?
frame-ancestors is the standard replacement and takes priority in browsers that support it, which is all current ones. Sending X-Frame-Options as well is harmless and covers very old clients, so many checklists keep both. If you send both, keep them consistent, for example frame-ancestors none with X-Frame-Options DENY.
Do security headers replace input validation and output encoding?
No. Headers are a second line of defense that limit what the browser will do if an injection slips through. Content-Security-Policy can stop an injected script from running, but the underlying bug is still there and still exposes the data or actions the page controls.
Sources
- MDN Web Docs: Practical implementation guides (Security headers)developer.mozilla.org
- OWASP HTTP Headers Cheat Sheetcheatsheetseries.owasp.org
- W3C Content Security Policy Level 3w3.org
- RFC 6797: HTTP Strict Transport Securityrfc-editor.org
- WHATWG Fetch Standard: X-Content-Type-Optionsfetch.spec.whatwg.org
- W3C Referrer Policyw3.org
- nginx: ngx_http_headers_module add_headernginx.org
- Helmet.jsgithub.com
- Next.js: headers configurationnextjs.org
Related
Content-Security-Policy Header: Directives, Nonces and Console Errors
Content-Security-Policy (CSP) limits what a page may load or run. Directives, nonces, strict-dynamic, Report-Only rollout, console errors, helmet and Next.js.
Strict-Transport-Security (HSTS) Header: Setup and Preload
Strict-Transport-Security (HSTS) forces HTTPS for a set time. max-age, includeSubDomains, preload rules, nginx and Apache config, and how to roll it back.
Cookie Security: HttpOnly, SameSite, and Secure Flags
A comprehensive guide to understanding and implementing secure HTTP cookies to protect against XSS, CSRF, and session hijacking attacks.
Permissions-Policy Header
Learn how the Permissions-Policy header controls which browser features and APIs can be used in your site and embedded iframes. Enhance security and privacy.