Skip to content
AscendQ.ca — Websites, Apps & SEO Systems

Security 9 min read

How to Fix "Missing Security Headers" (HSTS, CSP, X-Frame-Options, Permissions-Policy)

securityheaders.com — Grade: D
Missing Headers:
Strict-Transport-Security
Content-Security-Policy
X-Frame-Options
Permissions-Policy

HTTP security headers are instructions your server sends with every response, telling the browser what it may and may not do with your pages: whether the site can be framed by other sites, whether it must always use HTTPS, which sources of scripts and styles are trusted, which device APIs are allowed. Browsers enforce them strictly — which makes headers the rare security control that costs nothing at runtime and takes minutes to deploy.

That is also why scanners treat missing headers as an easy red flag: there is no good excuse. But there is one real hazard on the path to a clean grade — the Content-Security-Policy. HSTS, X-Frame-Options, and Permissions-Policy are essentially free to add. CSP actively blocks anything you did not whitelist, so a policy copied from a tutorial can take down your fonts, your analytics, your inline scripts, and your checkout in one deploy. This guide gives you the three free wins first, then a pragmatic CSP that hardens without breaking — the exact approach we used to take two production sites from a failing grade to an A this week.

You're In The Right Place If…

  • securityheaders.com (or a pen test / client security review) reports missing HSTS, CSP, X-Frame-Options, or Permissions-Policy
  • Lighthouse Best Practices or an enterprise procurement checklist flags header hardening
  • Your site can be loaded inside an iframe on any other domain (clickjacking exposure)
  • You added a strict CSP from a tutorial and parts of your site stopped working

What Each Missing Header Actually Protects Against

  • Strict-Transport-Security (HSTS): tells browsers to ALWAYS use HTTPS for your domain — even if a user types http:// or clicks an http link — for the next year. Kills SSL-stripping downgrade attacks. Zero breakage risk if your site is already HTTPS-only
  • X-Frame-Options: SAMEORIGIN: forbids other sites from loading yours in an iframe. This is the clickjacking defense — attackers overlay your framed page with invisible buttons so users click things they can't see
  • Permissions-Policy: declares which browser APIs your site uses. camera=(), microphone=(), geolocation=(), payment=() means "none" — so even if a malicious script sneaks in, it can't request the camera or track location
  • Content-Security-Policy (CSP): the big one — a whitelist of where scripts, styles, images, and frames may load from. Properly configured, it neutralizes most XSS attacks. Carelessly configured, it neutralizes your website

The Three Free Wins: Add These Today, Nothing Breaks

These values are safe for virtually every HTTPS site — no audit of your own code required (the two bonus headers below the requested four are equally free and scanners award them too):

Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Frame-Options: SAMEORIGIN
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin

CSP: The Pragmatic Policy Vs. The Perfect One

A maximum-strictness CSP whitelists every individual domain and replaces every inline script with nonces or hashes. It is the gold standard — and it is a project, not a header. Most real sites use inline scripts (analytics snippets, framework islands, tag managers), and a strict CSP breaks all of them the moment it ships.

The pragmatic middle: a policy that locks the attack surfaces that matter most — framing (clickjacking), plugins/objects, base-tag hijacking, non-HTTPS sources — while still allowing your own inline scripts and HTTPS-loaded assets. It scores well, blocks real attack classes, and cannot break a working site:

Content-Security-Policy: default-src 'self' https: data: 'unsafe-inline'; frame-ancestors 'self'; base-uri 'self'; object-src 'none'
  • frame-ancestors 'self' — the modern replacement for X-Frame-Options (keep both; older browsers only know the old one)
  • object-src 'none' — no Flash/plugin embeds, a classic injection vector
  • base-uri 'self' — stops injected <base> tags from redirecting all your relative URLs to an attacker's server
  • https: — every asset must load over HTTPS; plain-http injection is dead
  • The honest trade-off: 'unsafe-inline' means inline-script XSS isn't blocked by CSP itself. Tighten to nonces later if you handle sensitive data — as a deliberate project with testing, not a copy-paste

Where To Set Headers, Per Platform

  • Cloudflare Pages (static): create a _headers file in your build output — a /* block with one header per line applies site-wide. If your project uses an advanced-mode _worker.js, set the headers on responses in the worker instead (the _headers file does not cover worker-served responses)
  • Netlify: same _headers file format, or the [[headers]] block in netlify.toml
  • Vercel: the "headers" array in vercel.json
  • nginx: add_header directives in the server block (add "always" so they apply to error responses too)
  • Apache: Header always set … lines in the vhost or .htaccess
  • Behind a CDN? Set headers at the LAST hop the visitor touches — headers set on your origin can be dropped or overridden by the edge layer in front of it

HSTS Fine Print Worth Knowing Before You Ship It

HSTS is a one-way door for the duration of max-age: once a browser sees it, it will refuse plain HTTP for your domain until the timer expires. That is the point — but it means two checks first. One: everything on the domain is genuinely HTTPS (any http-only corner becomes unreachable). Two: includeSubDomains covers EVERY subdomain — staging, internal tools, that old app on legacy.yourdomain.com — all of them must be HTTPS too.

You will also see "preload" mentioned: it hard-codes your domain into the browsers' built-in HSTS lists, closing the tiny first-visit gap. It is genuinely permanent (removal takes months and browser-vendor cooperation), so treat preload as a separate, deliberate decision after HSTS has run cleanly for a while.

The Fix, Step By Step

  1. 1

    Scan and get your baseline

    Run your domain through securityheaders.com and note the grade and the missing list. Screenshot it — the before/after is satisfying and useful evidence for client security reviews.

  2. 2

    Ship the free four (plus two bonus) first

    HSTS, X-Frame-Options, Permissions-Policy, X-Content-Type-Options, and Referrer-Policy carry no breakage risk on an HTTPS-only site. Add them in your platform's header mechanism and deploy.

  3. 3

    Add the pragmatic CSP

    Use the policy above: locked framing, no objects, protected base-uri, HTTPS-only sources, inline scripts still allowed. This is the version that cannot take your site down.

  4. 4

    Verify on the live site, not in your config

    curl -sI https://yoursite.com | grep -i "strict\|frame\|security\|permissions" — confirm every header actually arrives at the browser (CDNs and proxies can strip them). Then click through your site: forms, checkout, anything with embeds.

  5. 5

    Re-scan, and schedule the tightening

    You should now score an A. If you handle payments or sensitive data, put "CSP nonce migration" on the roadmap as a real project — moving off unsafe-inline is the last mile, done deliberately with testing.

From the trenches

How we hit this on a real production site

This week a routine securityheaders.com scan on a freshly launched site came back flagged: no HSTS, no CSP, no X-Frame-Options, no Permissions-Policy. Same story on its sister site — both fast, modern, static-hosted builds with perfect Lighthouse scores. Speed and security headers are entirely separate axes: a 100/100 site can still be framed by any attacker page on the internet.

The fix took one deploy per site, but the two sites needed DIFFERENT mechanisms on the same host (Cloudflare Pages): the purely static site got a _headers file emitted by its build — while the site running an advanced-mode worker had to set headers inside the worker itself, because the _headers file does not apply to worker-served responses. That platform subtlety is the kind of thing that makes people think their headers "don't work".

For CSP we deliberately chose the pragmatic policy over the perfect one: both sites use inline scripts by design, so a strict nonce-based CSP would have meant reworking every page for a marginal scoring gain. Locked framing, no objects, protected base-uri, HTTPS-everywhere — shipped, verified with curl on the live domains, grade A, nothing broken. Total time including verification: under an hour for both sites. If your scan is showing red, send it to us — this is one of the fastest security wins that exists.

Frequently Asked Questions

Do missing security headers affect my Google rankings?

Not directly — headers are not a ranking signal. But they show up in every security review, procurement checklist, and pen test, and clickjacking/XSS incidents that headers prevent absolutely can hurt your business and reputation.

My headers are set in my server config but the scanner still says missing. Why?

Something between your origin and the visitor is dropping them — usually a CDN, proxy, or platform edge. Verify with curl -sI against the live domain, and set headers at the outermost layer the visitor actually touches.

X-Frame-Options vs frame-ancestors — which one do I need?

Both. frame-ancestors (in CSP) is the modern standard and supports allowing specific domains; X-Frame-Options is the legacy header some older browsers and scanners still expect. They coexist safely at SAMEORIGIN / 'self'.

Will the pragmatic CSP protect me from XSS?

Partially. It blocks framing, plugin, base-tag, and insecure-transport vectors, but 'unsafe-inline' means injected inline scripts are not blocked by CSP itself. Full inline-XSS protection requires nonce/hash-based CSP — a worthwhile project for sites handling sensitive data, done with proper testing.

Should I add HSTS preload?

Only after plain HSTS (with includeSubDomains) has run cleanly for a few months and you are certain every subdomain is HTTPS-forever. Preload is effectively permanent — great for mature setups, premature for new domains.

Still stuck? Send us the exact error.

Paste the exact error message you're seeing and where it happens. We'll take a look — if it's quick we'll point you at the fix, and if it's deeper we'll tell you honestly what it takes.

Reach the Summit

Rather have someone just fix it?

This is literally what we do all day — websites, SEO, performance, and the weird errors in between.

Start the Conversation