Start a project

Article · Web security · HTTP headers · CSP · 11 min read

Security headers and CSP checklist

HTTP security headers tell the browser how to treat your responses: whether to force HTTPS, whether the page may be framed, which scripts may run, which device features you do not intend to use. They close particular doors. They are not a product that makes an application “secure”, and a copied Content-Security-Policy can break a site as quickly as it can help one. The safe way to add them on a small site is a short core set, then a Content-Security-Policy that you run in report-only until the reports are quiet.

On this page 10 sections
  1. Headers that close doors, not a security product
  2. The core set: HSTS, referrer, framing, content type
  3. Permissions-Policy and the features you do not use
  4. CSP in report-only first
  5. Nonces, hashes and the unsafe shortcuts to avoid
  6. Cookies, HTTPS and mixed content
  7. How to read a violation report
  8. A rollout checklist
  9. Key takeaways
  10. Frequently asked questions

I set these on leej.uk at the edge, and I built the Security Headers checker, the CSP Evaluator and the CSP Report Analyser so I could read a policy before it went enforcing. The checklist below is the rollout I trust, not a score to game.

Headers that close doors, not a security product

A header is a precise instruction. X-Content-Type-Options: nosniff stops a browser from guessing a different MIME type. Content-Security-Policy can stop an unexpected script from running, if the policy is correct. Neither will fix a leaked API key, an open form, or a server you have not patched.

Treat them like a privacy manifest: declare what you actually use, and do not claim a tighter world than the page can live in. If the site needs a particular analytics script, the policy must say so, or you must drop the script. A policy that only works because you added 'unsafe-inline' and 'unsafe-eval' everywhere is a slogan.

Check the live response, not the configuration you think you deployed. The HTTP Header Inspector is the habit: look at the headers a client actually received, including after redirects.

The core set: HSTS, referrer, framing, content type

These four are a reasonable baseline for a small HTTPS site.

Strict-Transport-Security tells the browser to use HTTPS for later visits. Only enable it when the host already works on HTTPS and when HTTP already redirects there. Start with a modest max-age (for example a few minutes or a day) and raise it once you are sure you will not need to go back to HTTP. includeSubDomains applies the rule to every subdomain; do not set it until those subdomains are also ready. preload is a further step that asks browsers to hard-code the host into a preload list. That is powerful and slow to undo. Do not add preload or submit the host to a preload list until you have read the current criteria at hstspreload.org and MDN’s HSTS documentation, and you are willing to keep every subdomain on HTTPS.

Strict-Transport-Security: max-age=86400

Later, when you are confident:

Strict-Transport-Security: max-age=31536000; includeSubDomains

Referrer-Policy controls what other sites see in the Referer header when someone follows a link. strict-origin-when-cross-origin is a common, conservative default: a full URL on your own site, origin-only on HTTPS cross-origin links, and nothing when downgrading to HTTP. Tighter policies are fine if they still suit your own analytics or payment flows.

Referrer-Policy: strict-origin-when-cross-origin

Framing. Stop other sites from embedding your pages in an iframe unless you have a reason to allow it. The modern directive is CSP’s frame-ancestors. X-Frame-Options is the older header, still useful for clients that do not implement frame-ancestors. If you send both, they should agree. DENY or frame-ancestors 'none' is the usual choice for a site that is not meant to be framed; SAMEORIGIN or frame-ancestors 'self' if your own origin needs to embed itself.

X-Frame-Options: SAMEORIGIN
Content-Security-Policy: frame-ancestors 'self'

You can keep frame-ancestors in the main CSP once you have one, rather than as a separate stub policy.

X-Content-Type-Options: nosniff should be on, and your Content-Type values should be correct, so the browser does not have to guess. This matters for scripts and styles especially.

X-Content-Type-Options: nosniff

X-XSS-Protection is legacy and can be ignored or turned off; do not treat it as a modern defence. Other cross-origin isolation headers (Cross-Origin-Opener-Policy, Cross-Origin-Embedder-Policy, Cross-Origin-Resource-Policy) are useful in specific cases and can break third-party embeds and scripts. Add them when you know you need isolation, not as part of a copied starter pack.

Permissions-Policy and the features you do not use

Permissions-Policy (the successor to Feature-Policy) tells the browser which powerful features the page, and the frames it embeds, may use. If you do not need the camera, microphone, geolocation, payment, or USB, say so. That is a close cousin of declaring unused entitlements in an app: a smaller surface, and a clearer story when something asks.

A cautious default for a text-led site might look like this (adjust to the features you actually use, and check the current directive names, which have evolved):

Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=()

If a page later needs geolocation, open it on that origin or that document only, rather than loosening the site-wide header in a hurry. Do not disable a feature you already rely on and then spend an afternoon chasing a “permission denied” in a map widget you forgot about.

CSP in report-only first

Content-Security-Policy is the powerful, sharp header. It can block scripts, styles, images, connections, frames and more. The first time you add it in enforcing mode, you will usually break your own site: an inline script, a typography stylesheet on a CDN, a tool that loads a worker, or a data-URI image.

Start with Content-Security-Policy-Report-Only and a reporting endpoint you can read. The policy should describe the site you intend, not the loosest policy that silences reports. A sketch:

Content-Security-Policy-Report-Only: default-src 'self';
  img-src 'self' data:;
  font-src 'self';
  style-src 'self';
  script-src 'self';
  connect-src 'self';
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'self';
  report-to csp-endpoint

Then use the site as a person would: every template, every tool, the light and dark theme, the forms, the embeds you actually want. Read the reports. Widen a directive only when the blocked URL is a resource you accept. If the blocked URL is a surprise, find out why it is there before you allow it.

The CSP Evaluator will point out obviously weak directives. It cannot tell you whether a connect-src you added is a vendor you still want. Stay in report-only until a normal week of use produces only reports you understand and have decided to keep or to fix in the page.

When you switch to Content-Security-Policy, keep the report-only header for a while too if you can, so a tightening still has a safety net. Roll this out the way I maintain an iOS app after launch: one controlled change, watched, then the next.

Nonces, hashes and the unsafe shortcuts to avoid

Inline scripts and styles are the usual reason a first policy fails. You have three honest approaches:

What to avoid, unless you are in a temporary, documented exception:

Third parties will push you towards these shortcuts. Sometimes the honest answer is to drop the third party, or to load it only on one page with a tighter, separate policy, rather than to weaken the whole site. That is the same scope discipline as a careful App Store launch: do not smuggle a dangerous exception in at the last minute because a widget expected it.

Cookies, HTTPS and mixed content

Headers will not save a cookie that is too wide, or a page that still loads a script over HTTP.

CORS is a separate browser rule for cross-origin requests, covered in the CORS and preflight guide. Caching headers are also separate; a Cache-Control mistake will not be fixed by CSP, and is covered in the HTTP caching guide.

How to read a violation report

A CSP report is a JSON payload describing what was blocked or would have been blocked. Fields you will use, under the older report-uri format or the newer Reporting API, typically include the document URL, the directive that fired, the blocked URL, and whether the policy was enforcing or report-only. Names have shifted between versions, so look at a real payload from your own endpoint rather than memorising a schema from a blog.

When a report arrives, ask:

  1. Is this a URL I expected, on a page I expected? If yes, allow it in the tightest directive that will work, or change the page to load it differently.
  2. Is this my own inline script or style? If yes, move it, nonce it, or hash it. Do not jump to 'unsafe-inline' as the first fix.
  3. Is this an extension, a client toolbar, or a password manager injecting into the page? Those reports are noise in the field. Do not widen the policy to please a random extension.
  4. Is this a typo in the policy, or a stale policy on a cached HTML page? Check the header on the live response.
  5. Is this a surprise third party? If you did not add it, find the script that did.

The CSP Report Analyser is a way to group a pile of reports so you can see the repeated pairs of document and blocked URL. One unique report from your own walkthrough is often more useful than a thousand field reports you have not sampled.

A rollout checklist

These headers also belong on a technical SEO launch checklist for a new site. Revisit the list when you add a third party, a new subdomain, or a payment or map widget. A policy that is not in the same release habit as the HTML will drift, and an enforcing CSP that no longer matches the page is an outage.

Key takeaways

If you want a second pair of eyes on a CSP that only works with 'unsafe-inline', tell me about it.

Frequently asked questions

Which security headers does a small website need?

A practical baseline is HTTPS with HSTS (once the host is ready), Referrer-Policy, X-Content-Type-Options: nosniff, a framing policy (frame-ancestors, and X-Frame-Options if you still send it), a Permissions-Policy that turns off unused features, and a Content-Security-Policy rolled out carefully. Add stricter cross-origin isolation headers only when you know you need them.

How do I add a Content-Security-Policy without breaking the site?

Use Content-Security-Policy-Report-Only first, walk every template, and fix or allow the real violations you see. Move or nonce inline scripts rather than starting with 'unsafe-inline'. Switch to enforcing when a normal week of use is quiet, and keep reporting on so later changes still have a net.

What is the difference between CSP report-only and enforcing?

Report-only sends reports, or would-be violations, without blocking the resource. Enforcing blocks the resource and can also report. Report-only is how you learn whether a policy matches the site. Enforcing is the policy that actually protects visitors, and the one that will break a page if it is wrong.

Do I still need X-Frame-Options if I have CSP frame-ancestors?

frame-ancestors is the modern control and is more precise. X-Frame-Options still helps older clients that do not implement frame-ancestors. If you send both, they should mean the same thing. Once you have a CSP, put frame-ancestors in that policy rather than inventing a second, conflicting rule.

How do I add HSTS correctly?

Only after the site works entirely on HTTPS. Start with a short max-age, then raise it. Add includeSubDomains only when every subdomain is ready. Treat preload as a separate, hard-to-reverse step, and read the current preload requirements before you opt in. Check the live header after each change.

Written by Lee Johnson. Published 10 November 2026.

All articles · Swift A–Z · Apps

Questions, or an app in mind?

Let's bring your ideas to life. Whether you're envisioning a game-changing Apple Watch app or a transformative iOS experience, I'm here to make it happen.

Start a project