Every response from your server can give the browser instructions: use HTTPS only, don't run scripts you didn't authorize, don't let another site show you inside an iframe. These are HTTP security headers. They are cheap to set up, but a bad one can leave your site blank or, in the case of HSTS, lock your visitors out until the period you set expires.
Chasing a grade isn't the point either. Mozilla's HTTP Observatory warns in its own documentation that its grade ranges are "essentially arbitrary", and securityheaders.com explains that its score reflects how many security headers your site sends. A high grade does not guarantee they are configured correctly for your site.
This guide covers what to set, what no longer helps, how to deploy without breaking anything and how to check it, based on MDN, OWASP, the W3C, the IETF and Chrome, consulted on November 4, 2026.
Content-Security-Policy: the one that protects most and breaks most
CSP defines what code the page may run. It is mainly a defense against cross-site scripting (XSS) and also against clickjacking, but OWASP makes clear it should not be your only defense against XSS.
Strict policy with a nonce
Maintaining a policy built on allowlists of domains is laborious and, according to web.dev, it can be bypassed in most configurations. web.dev, MDN, OWASP and the W3C recommend a "strict CSP" instead:
Content-Security-Policy:
script-src 'nonce-{RANDOM}' 'strict-dynamic' https: 'unsafe-inline';
object-src 'none';
base-uri 'none';- The nonce is a random value the server generates for each response and repeats in every legitimate
<script nonce="...">. web.dev asks for a cryptographically strong value, ideally 128 bits or more, generated fresh every time. If it can be guessed, it doesn't protect anything. 'strict-dynamic'lets an authorized script load others. It makes third-party tools easier to use, although the specification recommends avoiding it when possible.https:and'unsafe-inline'are only fallbacks for older browsers. Browsers that understand nonces and'strict-dynamic'ignore them.object-src 'none'blocks plugins andbase-uriprevents injected<base>tags. The specification accepts'none'or'self'; use'self'if your site has its own<base>tag.
A nonce requires generating HTML on the server. For static sites or cached pages, web.dev recommends hashes ('sha256-...') of each inline script. And before enabling it, move handlers such as onclick="..." and javascript: links into JavaScript files: a strict CSP blocks them.
Test first in Report-Only mode
Content-Security-Policy-Report-Only evaluates the policy without blocking anything and reports what it would have blocked. According to MDN, without the report-to directive no reports are sent. The destination is declared in another header:
Reporting-Endpoints: csp="https://example.com/csp-reports"
Content-Security-Policy-Report-Only: script-src 'nonce-{RANDOM}' 'strict-dynamic' https: 'unsafe-inline'; object-src 'none'; base-uri 'self'; report-to csp; report-uri /csp-reportsreport-uri is deprecated, but MDN recommends declaring both until report-to works in every browser. The specification allows sending an enforced policy and a report-only one at the same time.
frame-ancestors against clickjacking
frame-ancestors 'none' stops any site from showing your page in an iframe; 'self' allows it only for your own origin. MDN recommends 'none' unless you need your site to be embeddable.
Watch out: in a <meta> tag the specification does not accept frame-ancestors, report-uri, sandbox or Report-Only mode. They must be sent as HTTP headers.
Strict-Transport-Security and preload
HSTS tells the browser to use only HTTPS with your domain for max-age seconds and not to let the user bypass certificate errors. It is defined in RFC 6797, from November 2012, with two directives: max-age and includeSubDomains.
- It only counts over HTTPS. The RFC requires the browser to ignore the header if it arrives over HTTP.
- The first visit is exposed. Until the browser has received the header, a first request over HTTP can be intercepted. The preload list exists to close that gap.
includeSubDomainscovers every subdomain, including old ones that still lack HTTPS.
The preload directive is not part of the RFC. The list is managed at hstspreload.org, a service hosted by Google, and according to MDN all browsers use it. To get in, the domain must serve HTTPS on all its subdomains, including www if it exists, and send a max-age of at least 31536000 seconds (one year) with includeSubDomains and preload.
The risk is in getting out of the list. hstspreload.org warns that inclusion cannot easily be undone: removal can take 6 to 12 weeks to reach most Chrome users, and longer for other browsers. It also asks that templates not include preload by default: don't copy it from an example.
On top of that, hstspreload.org recommends HSTS but does not recommend preloading: its added benefit is minimal compared with HSTS itself. Consider it only if you accept that getting off the list takes months.
The rollout hstspreload.org proposes raises max-age in stages: 300 seconds (5 minutes), 604800 (one week) and 2592000 (one month), waiting out the full period of each stage before moving to the next. If something goes wrong, max-age=0 disables HSTS, but only in browsers that visit you again over HTTPS.
Three simple headers
X-Content-Type-Options: nosniffmakes the browser respect the declaredContent-Type. It blocks scripts and stylesheets that arrive with another type, so first check that your server declares them correctly.Referrer-Policy: strict-origin-when-cross-originsends the full URL within your site, only the origin to other HTTPS sites and nothing when moving to HTTP. It is already the browser default, but OWASP recommends declaring it. If your URLs carry sensitive data,no-referrersends nothing.Permissions-Policydisables features you don't use, such as camera, microphone or geolocation, including inside third-party iframes. MDN flags it as experimental with partial support.
Cross-origin isolation: COOP, CORP and COEP
Cross-Origin-Opener-Policy: same-originputs your page in a browsing context group separate from windows of other origins, which lose the reference throughwindow.opener. If you use third-party sign-in or payment popups, MDN points tosame-origin-allow-popupsto keep that reference.Cross-Origin-Resource-Policystates who can embed your resources. web.dev recommends sending it on all of them, because an attacker can load them from another site to infer information. OWASP suggestssame-site; usecross-originon resources other sites need to load.Cross-Origin-Embedder-Policy: require-corpis only needed if you require cross-origin isolation, for example to useSharedArrayBuffer. OWASP warns that it blocks cross-origin resources that aren't configured correctly. If you don't need it, don't enable it.
Cookies
Session cookies are protected in Set-Cookie itself:
Set-Cookie: __Host-session=VALUE; Path=/; Secure; HttpOnly; SameSite=LaxSecure: the cookie only travels over HTTPS.HttpOnly: JavaScript can't read it, which reduces the impact of XSS. It is still sent with the site's requests.SameSite:Strictsends it only on requests from your own site;Laxalso on some navigations from other sites;Noneon all of them and requiresSecure. Some browsers applyLaxif you don't declare it, so declare it.__Host-prefix: the browser only accepts the cookie if it hasSecure, comes from an HTTPS page, has noDomainand usesPath=/. WithoutDomain, the cookie is returned only to the host that set it, not to its subdomains.
What you should no longer send
X-XSS-Protection: MDN marks it as deprecated and warns that in some cases it creates XSS vulnerabilities. OWASP recommends not sending it or sending it as0, and using CSP.X-Frame-Options: it is not deprecated, butframe-ancestorsreplaces it in browsers that support it. If you send both andframe-ancestorsis in an enforced policy (not Report-Only), the CSP specification saysX-Frame-Optionsis ignored. You can keepDENYfor older browsers;ALLOW-FROMno longer works.Expect-CT: according to MDN it has been mostly obsolete since June 2021, and Chromium deprecated it as of version 107 because it now enforces Certificate Transparency by default. OWASP: don't use it.Public-Key-Pins: Chromium removed it in 2018 and no modern browser supports it, according to OWASP. Remove it if it's still there.ServerandX-Powered-Bywith versions: OWASP recommends removingX-Powered-Byand leavingServerwithout useful details, while noting there are other ways to fingerprint your stack.
How to deploy them without breaking your site
- Take inventory of third-party scripts, iframes and popups.
- Start with what rarely breaks anything:
nosniff,Referrer-Policy,Permissions-Policyandframe-ancestors. - Keep the script CSP in Report-Only and review the reports before enforcing it.
- Raise HSTS in stages;
preloadis optional and not recommended by default. - Test errors and redirects, not just the home page.
Nginx
add_header only acts on some response codes (200, 201, 204, 206, 301, 302, 303, 304, 307 and 308) unless you add always. Also, a block that defines its own add_header stops inheriting those from the level above. Since version 1.29.3, the add_header_inherit directive lets you change that rule.
server {
listen 443 ssl;
server_name example.com;
server_tokens off; # hides the nginx version
add_header Strict-Transport-Security "max-age=300; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "object-src 'none'; base-uri 'self'; frame-ancestors 'none'" always;
location /downloads/ {
# This add_header cancels inheritance: repeat the headers above here
add_header Cache-Control "no-store" always;
}
}Apache
With mod_headers, Header set (onsuccess by default) and Header always set use separate tables; only always applies to errors and redirects generated by Apache. To avoid duplicate headers, OWASP suggests removing the onsuccess version and setting it with always.
<VirtualHost *:443>
ServerName example.com
<IfModule mod_headers.c>
Header always set Strict-Transport-Security "max-age=300; includeSubDomains"
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Content-Security-Policy "object-src 'none'; base-uri 'self'; frame-ancestors 'none'"
# X-Powered-By can end up in either table
Header unset X-Powered-By
Header always unset X-Powered-By
</IfModule>
</VirtualHost>
# In the global configuration: Server only says "Apache"
ServerTokens ProdCloudflare Pages
The _headers file, with no extension, goes in the static asset folder that gets published; without a framework, directly in the build output folder. It allows up to 100 rules and 2,000 characters per line, a limit to watch with long CSP policies. Its headers are not applied to Pages Functions responses: those are set in code.
/*
Strict-Transport-Security: max-age=300; includeSubDomains
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Content-Security-Policy: object-src 'none'; base-uri 'self'; frame-ancestors 'none'How to check them
Chrome DevTools. In Network, select the request and open Headers: Response Headers shows what arrived, and there you can override a header locally to test it before deploying. The Issues panel reports CSP violations as well as cookie and COEP problems.
curl. curl -I requests headers only, using the HEAD method. To see those of a GET, on the home page, on an error page and over HTTP:
curl -s -D - -o /dev/null https://example.com/
curl -s -D - -o /dev/null https://example.com/does-not-exist
curl -sI http://example.com/ # should redirect to HTTPS, with no HSTSHTTP Observatory. It lives at developer.mozilla.org/en-US/observatory. It launched on MDN on July 2, 2024, and the old observatory.mozilla.org address redirects there. Each domain's scan history is public, and it is designed for websites, not APIs.
securityheaders.com. Still running; it was created by Scott Helme and is now part of the company Snyk. It grades from A+ to F based on how many security headers you send; an R means the response was a redirect.
No grade tells you whether your CSP really protects you: the reports do.
Commented base configuration
For a site that isn't embedded elsewhere and doesn't need cross-origin isolation. Adjust it to your case:
# HTTPS for one year, no preload. Get here after the test stages
Strict-Transport-Security: max-age=31536000; includeSubDomains
# Enforced from day one: no plugins, no foreign <base>, and nobody (not even your own site) can frame your pages
Content-Security-Policy: object-src 'none'; base-uri 'self'; frame-ancestors 'none'
# In testing: the strict script policy, with reports
Reporting-Endpoints: csp="https://example.com/csp-reports"
Content-Security-Policy-Report-Only: script-src 'nonce-{RANDOM}' 'strict-dynamic' https: 'unsafe-inline'; object-src 'none'; base-uri 'self'; report-to csp; report-uri /csp-reports
# The browser respects the declared Content-Type
X-Content-Type-Options: nosniff
# Only the origin to other sites
Referrer-Policy: strict-origin-when-cross-origin
# Features the site doesn't use
Permissions-Policy: camera=(), microphone=(), geolocation=()
# Window separated from other origins (same-origin-allow-popups if you use payment or OAuth popups)
Cross-Origin-Opener-Policy: same-origin
# Your resources, only for your site and subdomains
Cross-Origin-Resource-Policy: same-site
# Only for older browsers; frame-ancestors takes precedence
X-Frame-Options: DENY
# Session cookie
Set-Cookie: __Host-session=VALUE; Path=/; Secure; HttpOnly; SameSite=LaxChecklist
Check | Header |
|---|---|
Present on errors and redirects too ( | All |
In Nginx, every | All |
Strict CSP tested in Report-Only, with no | CSP |
| CSP |
| HSTS |
| HSTS |
Correct |
|
Session with | Cookies |
No active | Obsolete |
| All |
DevTools, | All |
Sources
- MDN · Content Security Policy (CSP)
- MDN · Content-Security-Policy-Report-Only and Reporting-Endpoints
- W3C · Content Security Policy Level 3 (Working Draft, October 27, 2026)
- web.dev · Mitigate cross-site scripting (XSS) with a strict Content Security Policy (CSP)
- web.dev · Security headers quick reference
- OWASP · HTTP Security Response Headers Cheat Sheet
- OWASP · Content Security Policy Cheat Sheet
- OWASP Secure Headers Project
- RFC 6797 · HTTP Strict Transport Security (HSTS)
- MDN · Strict-Transport-Security
- hstspreload.org and removal from the list
- MDN: X-Content-Type-Options, Referrer-Policy, Permissions-Policy, Cross-Origin-Opener-Policy, Cross-Origin-Embedder-Policy, Cross-Origin-Resource-Policy and Set-Cookie
- MDN: X-XSS-Protection, X-Frame-Options and Expect-CT
- nginx · headers module and server tokens directive
- Apache HTTP Server · headers module and ServerTokens
- Cloudflare Pages · Headers
- Chrome DevTools · Network features reference and Issues panel
- curl · manual
- MDN · HTTP Observatory, FAQ and Tests and scoring
- securityheaders.com, About and FAQ
If you want to review your site's headers and plan how to enable them without disrupting it, let's talk.