Missing Security Headers: HSTS, CSP and More

Which HTTP security headers every site should send, what each one protects against, and how to add them at the server, CDN or framework.

Updated

The short answer

Security headers are HTTP response headers that tell the browser how to protect visitors on your site: always use HTTPS, which scripts may run, how to treat file types and how much of the URL to share with other sites. A page missing them still loads, but it leaves the door open to downgrade attacks, injected scripts and leaked URLs. The fix is to send four headers, Strict-Transport-Security, Content-Security-Policy, X-Content-Type-Options and Referrer-Policy, globally from your server, CDN or framework.

What security headers are

They travel with every response, before the HTML. You can see them in Chrome DevTools under Network, then the page request, then Headers:

http
HTTP/2 200
content-type: text/html; charset=utf-8
strict-transport-security: max-age=63072000; includeSubDomains
content-security-policy: default-src 'self'; object-src 'none'; frame-ancestors 'none'
x-content-type-options: nosniff
referrer-policy: strict-origin-when-cross-origin

What each one does:

  • Strict-Transport-Security (HSTS) tells the browser to use HTTPS for your domain for a set time, even if someone types http:// or follows an old link. It takes effect after the browser's first secure visit. Adding the domain to the browsers' HSTS preload list closes that first visit gap.
  • Content-Security-Policy (CSP) lists where scripts, styles, images and frames may load from, which limits the damage of cross-site scripting. A policy built on nonces or hashes with 'strict-dynamic' is the strongest form, because long lists of allowed domains are often possible to bypass. Its frame-ancestors directive stops other sites from framing yours.
  • X-Content-Type-Options with its only valid value, nosniff, stops the browser from guessing a file's type, so an uploaded file cannot be run as a script.
  • Referrer-Policy controls how much of your URL is sent with requests to other sites, from link clicks to images and scripts. Modern browsers already default to strict-origin-when-cross-origin, which sends only the domain across sites. Sending the header makes that choice explicit, or lets you pick a stricter one such as same-origin.

Why it matters

  • Visitor safety. These headers block whole classes of attack that no amount of careful code fully prevents.
  • Trust and compliance. Security reviews, penetration tests and enterprise customers check for them first.
  • HTTPS that sticks. HSTS stops repeat visitors from ever landing on the HTTP version again. If you still serve mixed content, fix that first, so a stricter policy does not break pages that still load HTTP resources.
  • Not a ranking factor. These headers do not lift rankings directly. They protect your visitors and your reputation, which a hacked site loses quickly.

Why they are missing

  • Defaults. Most web servers and frameworks send none of them unless configured.
  • Set in one place only. Headers added to the app but not to static files, or to the CDN but not to the origin.
  • A CSP stuck in report-only mode. Content-Security-Policy-Report-Only logs violations but enforces nothing, and it is easy to forget to switch it on.
  • Hosting limits. Some hosted platforms decide the headers for you.

How to find missing security headers

  1. Check a single page. Run curl -I https://example.com or open the request in DevTools and look for the four header names.
  2. Check every page. UpCrawler flags Missing security headers on any page that does not send Strict-Transport-Security, Content-Security-Policy, X-Content-Type-Options or Referrer-Policy, and lists which ones are missing. A report-only policy does not count as a CSP. Checking every URL matters because headers often differ between the app, static files and pages served by a different backend.
  3. Check Lighthouse. The best practices section includes Ensure CSP is effective against XSS attacks and Use a strong HSTS policy, which also judge the values, not only whether the headers exist.

How to fix it

Set the headers once, globally, at the layer that serves every response: the CDN, the web server or the framework config. Start with the three that rarely break anything: HSTS, X-Content-Type-Options: nosniff and Referrer-Policy: strict-origin-when-cross-origin. Then roll out CSP carefully: ship it as Content-Security-Policy-Report-Only, fix the violations it reports, and switch to the enforcing Content-Security-Policy header once the reports are clean.

For HSTS, start with a short max-age such as one day, confirm every subdomain works over HTTPS, then raise it to at least a year. Add preload and submit the domain to the preload list only when you are sure, since leaving the list takes months.

Add the headers to every route with headers() in next.config.ts. This CSP is the starting point for apps without nonces, because Next.js renders inline scripts that a plain default-src 'self' would block:

ts
const isDev = process.env.NODE_ENV === 'development'

const csp = [
  "default-src 'self'",
  `script-src 'self' 'unsafe-inline'${isDev ? " 'unsafe-eval'" : ''}`,
  "style-src 'self' 'unsafe-inline'",
  "img-src 'self' blob: data:",
  "font-src 'self'",
  "object-src 'none'",
  "base-uri 'self'",
  "form-action 'self'",
  "frame-ancestors 'none'",
  'upgrade-insecure-requests'
].join('; ')

const nextConfig: NextConfig = {
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: [
          { key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains' },
          { key: 'Content-Security-Policy', value: csp },
          { key: 'X-Content-Type-Options', value: 'nosniff' },
          { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' }
        ]
      }
    ]
  }
}

Use the key Content-Security-Policy-Report-Only first, and add every third-party origin you load, such as analytics, payments and fonts. For a stricter policy, generate a nonce per request in proxy.ts and use 'nonce-…' 'strict-dynamic' in script-src. Nonces need dynamically rendered pages, so statically generated pages stay on the policy above.

The most reliable place is the web server or CDN, so the headers also cover images, uploads and cached pages. On Apache, add them in .htaccess:

apache
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains"
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"

On Nginx, use add_header ... always; in the server block. From a theme or plugin, the send_headers action can add them with PHP's header(), but cached pages served by a caching plugin or CDN may skip PHP entirely. Plugins and themes add many scripts, so build the CSP in report-only mode first.

On Shopify's hosted Online Store, Shopify controls the HTTP response and already sends HTTPS related headers. Themes cannot add or change response headers. What you can do:

  • Add <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests"> inside <head> in theme.liquid. Browsers apply it, but it is not a header, so header checks still report the CSP as missing.
  • Move to a headless storefront such as Hydrogen, where you control the server response and can build a full CSP with createContentSecurityPolicy.
Fix with AI

Let AI fix it, page by page.

Paste this prompt into AI with your project open. In UpCrawler, every affected page comes with a prompt like this, already filled in with its URL.

Prompt

Send Strict-Transport-Security, Content-Security-Policy, X-Content-Type-Options and Referrer-Policy under those exact header names, adding only the ones the response is missing. Set them globally at the server or CDN edge rather than per route. Content-Security-Policy-Report-Only is not CSP — it enforces nothing and does not resolve this issue, so promote an existing report-only policy to the enforcing header and keep report-only only alongside it when testing a stricter candidate.

Frequently asked questions

Which security headers are the most important?

Strict-Transport-Security and a strict Content-Security-Policy protect the most. X-Content-Type-Options and Referrer-Policy are one line each and almost never break anything, so add them first while you prepare the CSP.

Do security headers affect SEO?

Not directly. They are not a ranking signal. They keep your site and visitors safe, and a compromised site loses rankings and trust fast.

Is X-Frame-Options still needed?

The CSP frame-ancestors directive replaces it in modern browsers. Sending X-Frame-Options: DENY as well is harmless and covers very old browsers.

Will a Content-Security-Policy break my site?

A strict one can block scripts you rely on. That is why you start with Content-Security-Policy-Report-Only, watch which resources it would block, add the legitimate ones and only then enforce it.

Found this useful?

Share it with whoever runs your site’s SEO.

Related issues

All SEO issues
Ready when you are!Crawly, the UpCrawler mascot

Catch issues like this on every page.

UpCrawler crawls your whole site, groups affected pages by template and hands you the fix for your platform.

14 days freeNo cardNothing to install