Skip to content
Strynal, Digital Agency

Engineering 5 min read

Security Headers and Content Security Policy

Which HTTP security headers every site should set, how to write a Content Security Policy that holds up in production, and where the real trade-offs are.

By Strynal Team

Most servers ship with whatever HTTP response headers their framework chose at version 1.0 and never revisit them. Security headers are a handful of lines of server config that instruct the browser how to handle your content before a single byte of HTML is parsed. Setting them deliberately takes under an hour on almost any stack, and the coverage they give for that cost is hard to beat.

The headers worth setting first

Start with five headers that carry no meaningful downside, then tackle Content Security Policy separately because it needs more care.

Strict-Transport-Security

HSTS tells the browser to connect over HTTPS only, and to remember that for as long as max-age specifies. A visitor who types http://yoursite.com gets upgraded client-side before a request leaves their machine, so there is no plaintext leg to intercept.

Strict-Transport-Security: max-age=63072000; includeSubDomains; preload

includeSubDomains covers every subdomain, which is almost always correct. preload flags the domain for Google’s HSTS preload list; browsers ship with that list baked in, so even first-time visitors are protected. Before enabling preload, confirm that every subdomain genuinely serves HTTPS. Reverting off the preload list is a slow, manual process. If you want more background on the underlying protocol, our primer on HTTPS, SSL, and TLS covers how the handshake works.

X-Content-Type-Options

X-Content-Type-Options: nosniff

One value, no decisions. This stops browsers from guessing the MIME type of a response and executing something you served as text/plain as JavaScript. Set it and forget it.

X-Frame-Options

X-Frame-Options: DENY

Prevents your pages from loading inside an <iframe> on another origin. Clickjacking attacks work by placing an invisible frame over a button the user trusts. If you have a legitimate reason to allow specific origins, the modern equivalent is Content-Security-Policy: frame-ancestors 'self', which is more granular.

Referrer-Policy

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

Controls how much URL detail is passed in the Referer header when a visitor follows a link off your site. This value sends your origin (https://yoursite.com) on cross-origin requests but the full path on same-origin ones. You keep analytics fidelity internally without leaking internal paths or query strings to third-party destinations.

Permissions-Policy

Formerly Feature-Policy, this header lets you switch off browser features your site does not use.

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

An empty () means no origin can use that feature through your pages. If injected code ever runs, it cannot silently access the camera or location. List only the features you actively need; explicit is better than permissive.

Content Security Policy

CSP is the most powerful header here and the most commonly misconfigured. It defines which origins are allowed to load scripts, styles, images, fonts, and other resources. A browser enforcing a tight policy will refuse to execute an injected script, even if an attacker managed to get code onto the page.

A Content Security Policy does not prevent XSS from happening. It stops the browser from executing the injected code, which is what limits the damage.

The value of that distinction is real. XSS is one of the most common web vulnerabilities, and a well-written CSP is often the difference between a researcher filing a report and an attacker draining sessions.

Start in report-only mode

Never deploy an enforcing CSP on a live site without testing it first. Use Content-Security-Policy-Report-Only with a report-uri or report-to endpoint. Violations are reported but not blocked, so you see what would break before anything actually does.

A starting policy for a typical marketing site looks something like this:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://www.googletagmanager.com;
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  font-src 'self' https://fonts.gstatic.com;
  connect-src 'self';
  frame-ancestors 'none';
  upgrade-insecure-requests;

Run this in report-only first. Fix the violations. Then switch it to enforcing.

The trade-offs you will hit

'unsafe-inline' for styles is the usual first concession. Many UI frameworks generate inline style attributes, and blocking them cold on a live site is visibly destructive. Moving styles out-of-line is the correct fix, but on an inherited codebase it can be weeks of work. For scripts the calculus is different: 'unsafe-inline' on script-src largely defeats the point of the header. Use nonces instead. A nonce is a cryptographically random value generated per request, stamped on every <script> tag your server renders, and declared in the CSP header. An injected script has no matching nonce and gets blocked.

upgrade-insecure-requests tells the browser to silently rewrite http:// sub-resource requests to https://. It is a good fallback for legacy content but not a substitute for fixing the URLs in your markup.

Third-party scripts are where CSP maintenance gets real. Chat widgets, A/B testing tools, tag managers, and analytics each potentially need a new allowlist entry. Audit your pages before writing the policy. And treat CSP as a header you revisit after every significant site change, not a one-time configuration.

Nonces versus hashes

If per-request rendering is on the table, nonces are the cleanest approach and are built into most modern server-side rendering frameworks. If you are on a static site generator and need to include inline scripts, hashes are the alternative: compute a SHA-256 of each inline script and list it in the policy. Change the script, update the hash. More maintenance, but it works without dynamic rendering.

Neither approach is painless. The payoff is that a working, enforcing CSP on a site with a clean script inventory is meaningfully harder to exploit than one running without it.

How Strynal approaches security headers

Security headers are part of the launch checklist on every project we deliver through our websites and apps practice. HSTS, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, and Permissions-Policy go on every site. CSP gets written during the build, not retrofitted afterward, because adding it to a site with years of accumulated inline scripts and third-party integrations is substantially harder than starting clean.

For sites we rebuild or redesign, the headers audit is a natural gate before the new version goes live. It costs very little at that point. The foundational security baseline we follow covers the broader picture: headers are one layer of a working posture, alongside TLS configuration, dependency hygiene, and access controls. A site redesign is also the right moment to consolidate third-party scripts, which makes writing a tight CSP much less painful than it would be mid-lifecycle.

If you are not sure where your site currently stands, run it through Mozilla Observatory. It is free, it takes thirty seconds, and the output names each missing header with enough context to act on immediately.