HTTP security headers: CSP, HSTS and the rest
Which HTTP security headers to set, with starting values, what each one breaks if you get it wrong and how to roll out CSP without taking the site down.
On this page 11 min read
- Summary: starting values and what can break
- Strict-Transport-Security (HSTS)
- Content-Security-Policy (CSP)
- X-Content-Type-Options
- Referrer-Policy
- Permissions-Policy
- X-Frame-Options or frame-ancestors
- Cross-Origin-Opener-Policy and Cross-Origin-Resource-Policy
- Headers you should no longer use
- A complete starter set
- How to check your site
The HTTP security headers that apply to almost every site are Strict-Transport-Security (forces HTTPS), Content-Security-Policy (limits scripts and who can show the page in an iframe), X-Content-Type-Options: nosniff, Referrer-Policy and Permissions-Policy. X-Frame-Options is still useful as a fallback for old browsers, and Cross-Origin-Opener-Policy and Cross-Origin-Resource-Policy isolate the page from other sites. X-XSS-Protection, Expect-CT and Feature-Policy should no longer be used. CSP takes the most work and breaks the most things; roll it out in report-only mode first.
This article covers browser behavior and the recommendations of MDN, OWASP and the W3C as of October 2026. It is part of the guide to website security basics, which covers HTTPS, exposed files, versions and cookies.
Summary: starting values and what can break
The values below are a common starting point, based on the references cited in each section. They are not the only correct ones: each site needs to adjust them to what it uses.
| Header | Starting value | What can break |
|---|---|---|
Strict-Transport-Security | max-age=63072000; includeSubDomains | Subdomains that do not have HTTPS yet become unreachable |
Content-Security-Policy | Policy with a nonce, object-src 'none', base-uri 'none' and frame-ancestors | Inline scripts, onclick handlers, eval(), third-party widgets |
X-Content-Type-Options | nosniff | Scripts and CSS served with the wrong Content-Type |
Referrer-Policy | strict-origin-when-cross-origin | Tools on another domain that rely on the full source path |
Permissions-Policy | camera=(), microphone=(), geolocation=() | Features the site or a partner iframe actually uses |
X-Frame-Options | DENY or SAMEORIGIN | Legitimate display of the site inside iframes |
Cross-Origin-Opener-Policy | same-origin, or same-origin-allow-popups if there is login or payment in a pop-up | OAuth and payment pop-ups that talk to the window that opened them |
Cross-Origin-Resource-Policy | same-site | Files on your domain used by other sites |
Strict-Transport-Security (HSTS)
HSTS tells the browser that the host can only be accessed over HTTPS for max-age seconds. Once it has received the header, the browser converts http requests to https on its own and does not let the user bypass certificate errors (MDN: Strict-Transport-Security). Three details matter:
The header only counts if it comes in an HTTPS response; sent over http, it is ignored.
It only protects from the second visit on, because the browser has to have received the header once. The browsers' preload list (
preload) closes that gap.includeSubDomainsextends the rule to every subdomain, present and future.
Strict-Transport-Security: max-age=63072000; includeSubDomainsMozilla recommends at least six months and suggests two years (63072000) (MDN: TLS practical guide); ASVS 5.0 asks for at least one year and, from level 2, the inclusion of subdomains (ASVS 5.0, requirement 3.4.1). Rollout recommendation: start with a short max-age, check that everything works and only then raise it, because the browser will remember the rule for the whole period. OWASP warns that a misconfigured HSTS or a certificate problem can lock users out for the entire max-age (OWASP HTTP Headers Cheat Sheet).
To get on the preload list, the site needs a valid certificate, must redirect http to https on the same host, must have HTTPS on every subdomain and must send a max-age of at least one year with includeSubDomains and preload. Getting off the list is slow (it can take months to reach users), and other browsers are not obliged to follow the removal (hstspreload.org). Only apply if every subdomain, including internal ones, will stay on HTTPS in the long term.
Content-Security-Policy (CSP)
CSP is a list of rules that tells the browser where the page can load scripts, styles, images and iframes from, and where it can send data. Its main use is to reduce the impact of XSS (cross-site scripting, injecting a script into the page): even if an attacker manages to insert a <script>, the browser does not run it if it does not meet the policy. It is an extra layer and does not replace fixing XSS flaws (web.dev: strict CSP).
Domain allowlist or strict policy with a nonce
There are two ways to write the script rule. The older one is to list allowed domains (script-src 'self' https://cdn.example.com). The one MDN and web.dev recommend is the so-called strict CSP, based on a nonce or hash (MDN: CSP guide; web.dev: strict CSP). ASVS 5.0 accepts both forms at level 2, as long as the policy has object-src 'none' and base-uri 'none', and requires a per-response nonce or hash at level 3 (ASVS 5.0, requirement 3.4.3).
A nonce is a random value the server generates for each response, puts in the header and repeats in the nonce attribute of every legitimate <script>. The browser only runs scripts that carry the right value. The nonce has to be unpredictable and different on every response, and it should be inserted by the templating system, never added to every <script> on the page indiscriminately, so that an injected script does not get stamped too (MDN: CSP guide).
Content-Security-Policy:
script-src 'nonce-R4nd0mPerResponse' 'strict-dynamic';
object-src 'none';
base-uri 'none';
frame-ancestors 'self'
<script nonce="R4nd0mPerResponse" src="/js/app.js"></script>'strict-dynamic' lets scripts created by an already trusted script run too. This allows third-party libraries and widgets to load their own files without you listing every domain (web.dev: strict CSP). For static or cached pages, web.dev recommends a hash instead of a nonce: 'sha256-...' calculated over the content of each inline script. A nonce saved into a cached page repeats for every visitor and becomes pointless.
frame-ancestors
The frame-ancestors directive defines which pages can show the site inside an <iframe>, which protects against clickjacking (when another site shows yours underneath fake buttons). MDN recommends 'none' for sites that do not need to be embedded (MDN: CSP guide). It does not inherit the value of default-src and does not work in a meta tag (MDN: frame-ancestors).
Content-Security-Policy: frame-ancestors 'none'
Content-Security-Policy: frame-ancestors 'self' https://partner.example.comReport-only mode
The Content-Security-Policy-Report-Only header applies the same syntax without blocking anything: the browser only logs and sends the violations (MDN: Content-Security-Policy-Report-Only). Both headers can be sent together; in that case the normal policy is enforced and the report-only one just produces warnings, which lets you test a stricter version without touching the one in force (MDN: CSP guide).
Reports go to the address defined in the Reporting-Endpoints header and referenced by the report-to directive. The older report-uri directive is deprecated, but MDN recommends declaring both until report-to works in every browser (MDN: CSP guide; W3C: CSP Level 3).
Reporting-Endpoints: csp="https://example.com/csp-reports"
Content-Security-Policy-Report-Only:
script-src 'nonce-R4nd0mPerResponse' 'strict-dynamic';
object-src 'none';
base-uri 'none';
report-uri https://example.com/csp-reports;
report-to cspHow to roll out CSP without taking the site down
List the scripts and resources of each page type (home, product, checkout, logged-in area) using the DevTools Network tab.
Choose a nonce if pages are generated on the server for each request, or a hash if they are static or cached (web.dev).
Adjust the code the policy will block: replace inline handlers such as
onclickwithaddEventListener, replacejavascript:links with event handlers and removeeval()(for example, in favor ofJSON.parse()) (web.dev).Publish the policy in
Content-Security-Policy-Report-Onlywith a reporting endpoint.Follow the reports long enough to cover every page type and flow, including checkout and seasonal campaigns, and fix or allow each legitimate origin.
Switch to
Content-Security-Policyand keep sending reports.Review the policy in the CSP Evaluator or in Lighthouse before and after the switch, as web.dev suggests.
What usually breaks
Inline scripts without a nonce or hash, including the ones plugins and themes print on the page.
Event attributes (
onclick,onload) andjavascript:links, which strict CSP blocks.Libraries that use
eval()ornew Function(). Allowing them with'unsafe-eval'weakens the policy (web.dev).A nonce on a cached page, which repeats the value and either makes scripts fail or leaves the protection with no effect.
With a domain allowlist policy: any new origin (a pixel, a chat, a font) stays blocked until someone updates the list.
X-Content-Type-Options
With nosniff, the browser stops guessing the file type from its content and uses the declared Content-Type. Script requests are blocked if the type is not a JavaScript type, and stylesheet requests if it is not text/css. This prevents, for example, a user-uploaded file served as text from being run as HTML or script (MDN: X-Content-Type-Options). ASVS asks for the header on every response (requirement 3.4.4).
X-Content-Type-Options: nosniffWhat breaks: files served with the wrong type, such as a .js delivered as text/plain. Before turning it on, check the Content-Type of your scripts and CSS, especially the ones generated dynamically.
Referrer-Policy
The Referer header tells the destination site where the visitor came from. If the address has data in the path or query string (a token, an email address, an order number), that data leaks to third parties. Referrer-Policy controls how much of that address is sent. When the site sets nothing, the browsers' current default is strict-origin-when-cross-origin: the full address to the same site, only the origin to other sites over HTTPS and nothing when the connection drops to HTTP (MDN: Referrer-Policy).
Referrer-Policy: strict-origin-when-cross-originOWASP recommends declaring the header instead of relying on the browser default and uses this value as its example (OWASP HTTP Headers Cheat Sheet); the OWASP Secure Headers Project's list of values goes further and suggests no-referrer (headers_add.json). no-referrer is more restrictive, but it can get in the way of analytics tools that depend on the traffic source. Avoid unsafe-url, which sends the full address even to HTTP destinations.
Permissions-Policy
Permissions-Policy turns browser features such as camera, microphone, geolocation and payment on or off, for the page and for the iframes inside it. () turns the feature off everywhere; self allows it only for the same origin; a quoted origin allows it for that domain (MDN: Permissions-Policy).
Permissions-Policy: camera=(), microphone=(), geolocation=()
Permissions-Policy: geolocation=(self "https://maps.example.com"), microphone=()OWASP's recommendation is to turn off everything the site does not use (OWASP HTTP Headers Cheat Sheet). MDN marks the header as experimental and says it does not work in some widely used browsers, so treat it as an extra protection, not the only barrier. What breaks: a partner iframe (map, video, payment) that needs a feature you turned off. On the iframe, the allow attribute grants the feature within the limit the header allows.
X-Frame-Options or frame-ancestors
X-Frame-Options was the first header against clickjacking. It accepts DENY (nobody can show the page in an iframe) and SAMEORIGIN (only pages from the same origin). The ALLOW-FROM value is obsolete and makes modern browsers ignore the whole header, and the header has no effect in a meta tag (MDN: X-Frame-Options).
The CSP frame-ancestors directive does the same with more options, such as allowing specific domains. The CSP specification says that when a policy with frame-ancestors is being enforced, the browser ignores X-Frame-Options (W3C: CSP Level 3, section 6.4.2.2). ASVS 5.0 treats X-Frame-Options as obsolete and asks for frame-ancestors (requirement 3.4.6). Recommendation: use frame-ancestors as the main rule and keep a consistent X-Frame-Options for old browsers. It does not get in the way of new ones, which already ignore it in that case.
Cross-Origin-Opener-Policy and Cross-Origin-Resource-Policy
Cross-Origin-Opener-Policy (COOP) decides whether your page shares its browsing context with windows from other sites that it opened or that opened it. With same-origin, a window from another domain is isolated and loses the window.opener reference. This breaks login (OAuth) and payment flows in pop-ups that send the result back to the original window; for those cases there is same-origin-allow-popups (MDN: Cross-Origin-Opener-Policy). ASVS asks for one of the two values on responses that render documents (requirement 3.4.8).
Cross-Origin-Opener-Policy: same-origin-allow-popupsCross-Origin-Resource-Policy (CORP) goes on the resource, not the page. It says which origins can load that file in no-CORS requests, such as <img> and <script>: same-origin, same-site or cross-origin (MDN: Cross-Origin-Resource-Policy). Mozilla lists CORP among the protections against speculative side-channel attacks (MDN: practical guides), and OWASP suggests same-site. What breaks: images, fonts or scripts on your domain used by other sites, such as a widget you distribute. Those files need cross-origin.
The pair Cross-Origin-Embedder-Policy: require-corp with COOP same-origin turns on cross-origin isolation, which features such as SharedArrayBuffer need (MDN). COEP blocks resources from other origins that do not allow their use through CORP or CORS (OWASP). Opinion: on a company website or store that does not need this isolation, COEP usually costs more (blocked ads, videos and widgets) than it delivers.
Headers you should no longer use
| Header | Status | What to do |
|---|---|---|
X-XSS-Protection | Not part of any specification and obsolete. MDN warns that the filter can create XSS flaws on sites that did not have them (MDN) | Do not send it, or send 0 to turn the filter off, as OWASP suggests; protect with CSP |
Expect-CT | Only Chromium implemented it, and it has considered it obsolete since version 107, because it already requires Certificate Transparency by default (MDN) | Remove it |
Feature-Policy | Renamed to Permissions-Policy, with a different syntax. In Chrome it only works as an alias, and Permissions-Policy wins when both are sent (Chrome for Developers) | Migrate to Permissions-Policy |
Public-Key-Pins | OWASP recommends not using it and removing it from production (OWASP) | Remove it |
A complete starter set
An example for a dynamically served site that is not embedded by other sites and has payment in a pop-up. How you configure it depends on the server (nginx, Apache), the CDN or the framework; the content of the headers is the same.
Strict-Transport-Security: max-age=63072000; includeSubDomains
Content-Security-Policy: script-src 'nonce-{generated per response}' 'strict-dynamic'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; report-to csp
Reporting-Endpoints: csp="https://example.com/csp-reports"
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
X-Frame-Options: DENY
Cross-Origin-Opener-Policy: same-origin-allow-popupsFor comparison, the OWASP Secure Headers Project publishes a stricter list, updated often, meant as a maximum reference and not as a configuration to copy without testing (headers_add.json).
How to check your site
In the terminal, curl -sI https://example.com/ shows the headers of a response. Repeat it for an inner page, an image file and an error response, because servers and CDNs do not always apply the same headers to everything. Mozilla's HTTP Observatory gives a score and explains each item. The WebVyze audit flags missing HSTS, CSP, X-Content-Type-Options and Referrer-Policy, a CSP that exists but does not stop injected scripts and a page that can be embedded by other sites, each with a fix guide. For stores, the CSP on the payment page also counts for PCI DSS; the article on requirements 6.4.3 and 11.6.1 explains how, and WebVyze's paid plans keep an inventory of scripts on checkout pages. WebVyze only reads public pages and does not change anything on the site.
Frequently asked questions
- Can I put the CSP in a meta tag instead of the header?
- Yes, with limits. The specification ignores the frame-ancestors, report-uri and sandbox directives when the policy comes from a meta tag, and report-only mode (Content-Security-Policy-Report-Only) does not work in a meta tag. For a complete policy, use the header.
- Do the headers need to be on every response or only on HTML pages?
- It depends on the header. ASVS 5.0 asks for HSTS, nosniff and frame-ancestors on every response and Cross-Origin-Opener-Policy on responses that render a document, such as HTML pages. Setting them on the server or CDN for every response is usually the simplest route.
- Does removing the Server and X-Powered-By headers improve security?
- It makes it harder to identify the technology in use, and OWASP recommends removing these headers or leaving them without useful information. It does not stop someone from finding out the technology by other means, and it does not replace updates.
- How do I receive CSP violation reports?
- The browser sends a JSON POST to the address declared in Reporting-Endpoints (report-to directive) or in report-uri. You need an endpoint that accepts and stores these submissions: a simple service of your own or a third-party tool.
See what your site needs to fix.
A free check by law, with the cause and the fix guide for each problem.
Keep reading
- Technical SEO
AI crawlers in robots.txt: search, user access and training
GPTBot, ClaudeBot, OAI-SearchBot, Google-Extended: what each AI crawler does, whether it respects robots.txt, and how to block training but stay in search.
- Technical SEO
Redirects: chains, loops and how to fix them
How Google handles 301, 302, 307 and 308, what chains and loops do to crawling and speed, and how to map, fix and test your redirects.
- Technical SEO
llms.txt: what it is, what it doesn't do and how to create one
llms.txt summarizes a site for AI agents. See the format, what Google and other companies say about it, when it's worth creating and how to write a good one.