Rustine

Security Headers: How to Check and Fix Them

Security headers are HTTP response headers that tell a browser how to handle your site: which scripts it can run, whether it can sit inside an iframe, and whether traffic must stay on HTTPS. They stop common attacks — cross-site scripting, clickjacking, MIME sniffing — before your own code even runs. Here's how to check yours in two minutes and add the missing ones to a Next.js, Vercel, or Netlify app.

Security Headers: How to Check and Fix Them
Contents
  1. What are security headers?
  2. The security headers that actually matter
  3. Content-Security-Policy (CSP)
  4. Strict-Transport-Security (HSTS)
  5. X-Content-Type-Options
  6. X-Frame-Options
  7. Referrer-Policy
  8. Permissions-Policy
  9. The ones you can ignore
  10. Cookies are headers too
  11. How to check your security headers
  12. The fixes, ready to paste
  13. Next.js (next.config.js or next.config.ts)
  14. Vercel (vercel.json)
  15. Netlify (netlify.toml)
  16. Don't ship a CSP that breaks your app
  17. Security headers on an AI-built app
  18. Where security headers fit in the bigger picture
  19. Frequently asked questions
  20. What are the security headers?
  21. How do I find missing security headers?
  22. Which security header is most important?
  23. Do security headers make my site fully secure?
  24. Is X-XSS-Protection still needed?
  25. Sources

Editorial illustration of a browser window with layered HTTP response headers

What are security headers?

Every time a browser loads a page, your server answers with a response: a status code, a body, and a set of HTTP headers. Most headers are neutral plumbing — content type, cache control, cookies. Security headers are the subset that change how the browser behaves as a security boundary.

They are standing instructions to the browser:

  • "Only run scripts from these places" (Content-Security-Policy)
  • "Only talk to me over HTTPS" (Strict-Transport-Security)
  • "Never let another site embed me" (X-Frame-Options)
  • "Don't guess what type of file this is" (X-Content-Type-Options)

A browser reads these before it renders anything, which is why they work even when a bug in your JavaScript doesn't. They are the cheapest hardening you can ship — and most AI-coded apps launch without them.

The security headers that actually matter

There are more than a dozen headers with "security" in their name. You need five, and a sixth is worth knowing. The rest are legacy or specialized.

Content-Security-Policy (CSP)

CSP is a whitelist of what a page is allowed to load and run: scripts, styles, images, fonts, connections. Injected JavaScript — the payload behind most cross-site scripting (XSS) attacks — is simply not on the list, so the browser refuses to run it.

It is the strongest header of the set, and the one most likely to break your app if you set it too strictly. Start wide, then tighten.

Strict-Transport-Security (HSTS)

HSTS tells the browser: "for the next two years, only ever connect to this domain over HTTPS." It stops protocol-downgrade attacks, where someone on the same network nudges a visitor onto an insecure connection.

The value to use is max-age=63072000; includeSubDomains; preload. 63072000 is two years in seconds. Only add preload once you are certain every subdomain is served over HTTPS — that flag makes the promise permanent and hard to undo.

X-Content-Type-Options

Set to nosniff, this stops the browser from guessing a file's type (MIME sniffing). Without it, a browser can decide that an uploaded file claiming to be an image is actually HTML, and execute it. It is one line, with no downside.

X-Frame-Options

This controls whether another site can embed your pages in an iframe, the basis of clickjacking — a transparent frame laid over a button so the visitor clicks something they can't see. DENY blocks all embedding; SAMEORIGIN allows only your own site. If you already use CSP, the modern equivalent is the frame-ancestors directive.

Referrer-Policy

When a visitor clicks a link off your site, the browser sends a Referer header telling the next site where they came from — sometimes the full URL with query parameters. strict-origin-when-cross-origin sends only your domain (never the path) to other sites, and the full referrer within your own. It stops sensitive values in URLs — reset tokens, search terms — from leaking to third parties.

Permissions-Policy

This restricts which browser features your site can use — camera, microphone, geolocation, autoplay — and whether it can grant them to iframes. A blank slate like camera=(), microphone=(), geolocation=() means: my site doesn't use these, so block them even if a script asks.

The ones you can ignore

  • X-XSS-Protection is legacy. Modern browsers ignore it, and old implementations could be abused. OWASP no longer recommends it — if you see it in a tutorial or a template, leave it out.
  • Expect-CT and Public-Key-Pins (HPKP) are deprecated. HPKP in particular could lock you out of your own site if misconfigured. Don't use either.
  • Server, X-Powered-By, and X-AspNet-Version are not security headers — they are information disclosure. They tell an attacker which stack you run. Remove them where your host lets you.

Cookies are headers too

Session cookies travel in the Set-Cookie response header, and three flags decide how safe they are: HttpOnly (JavaScript can't read the cookie, so XSS can't steal the session), Secure (only send over HTTPS), and SameSite (don't send the cookie on cross-site requests, which mitigates CSRF). If your app handles authentication, check that these flags are set where they apply. If you use Supabase, its auth token is kept in browser storage rather than a cookie by default, which makes your database rules — not a header — the real boundary.

How to check your security headers

You don't need to guess. Three ways:

  1. securityheaders.com — paste your domain and get a letter grade (A+ to F) plus a list of what's missing. It's the quickest read.
  2. MDN HTTP Observatory — Mozilla's scanner. It's stricter and explains each finding. Since 2016 it has run over 47 million scans across 6.9 million sites.
  3. curl — the terminal check, no browser needed:
bash
curl -I https://yoursite.com

Look for strict-transport-security, content-security-policy, x-content-type-options, and x-frame-options in the output. If they are absent, the browser is getting no instructions.

A scanner tells you what's missing. It doesn't write the fix. That's the part people put off.

Editorial illustration of a security checklist being reviewed

The fixes, ready to paste

These go in the config file your host reads at deploy time — not in your React components, where they would do nothing. Pick the one that matches your setup and paste it into your agent.

Next.js (next.config.js or next.config.ts)

Add this to next.config.js. Replace the CSP sources with the hosts your app actually loads from (your Supabase project, Stripe, analytics), then check that the app still loads and that curl -I https://yoursite.com shows content-security-policy.

js
// next.config.js
module.exports = {
  async headers() {
    return [
      {
        source: "/(.*)",
        headers: [
          { key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains; preload" },
          { key: "X-Content-Type-Options", value: "nosniff" },
          { key: "X-Frame-Options", value: "DENY" },
          { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
          { key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" },
          {
            key: "Content-Security-Policy",
            value: "default-src 'self'; script-src 'self' 'unsafe-inline' https://js.stripe.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self' https://*.supabase.co https://*.stripe.com",
          },
        ],
      },
    ];
  },
};

Vercel (vercel.json)

json
{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        { "key": "Strict-Transport-Security", "value": "max-age=63072000; includeSubDomains; preload" },
        { "key": "X-Content-Type-Options", "value": "nosniff" },
        { "key": "X-Frame-Options", "value": "DENY" },
        { "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" },
        { "key": "Permissions-Policy", "value": "camera=(), microphone=(), geolocation=()" }
      ]
    }
  ]
}

Netlify (netlify.toml)

toml
[[headers]]
  for = "/*"
  [headers.values]
    Strict-Transport-Security = "max-age=63072000; includeSubDomains; preload"
    X-Content-Type-Options = "nosniff"
    X-Frame-Options = "DENY"
    Referrer-Policy = "strict-origin-when-cross-origin"
    Permissions-Policy = "camera=(), microphone=(), geolocation=()"

Editorial illustration of a scan inspecting a web page's headers

Don't ship a CSP that breaks your app

CSP is the one header that can take your app down if it's wrong. The failure mode is quiet: the page loads, but scripts and styles silently stop running. Two ways to protect yourself:

  1. Start with report-only. Content-Security-Policy-Report-Only logs violations to a URL you choose but doesn't block anything. Ship it, watch the reports for a day, then switch to the enforcing header.
  2. Add sources as you discover them. A blank default-src 'self' breaks anything loaded from a CDN. If your analytics, fonts, or embeds come from elsewhere, add that host to the right directive.

One warning about script-src 'unsafe-inline': it's in the starter config above because many AI-generated apps inject scripts inline. It weakens CSP — an attacker who finds an injection point can run inline code. Treat it as temporary; remove it once your scripts move to files.

Security headers on an AI-built app

If you built your app with Lovable, Bolt, or v0, headers are easy to miss because the tool doesn't surface them. You won't find a "security headers" toggle in the builder UI. But the app still deploys somewhere — usually Vercel or Netlify — and that host is where the headers live. So the fix is the same file in the repo, not a setting inside the builder.

This matters more than it looks. AI coding assistants will happily generate a full app and skip every one of these headers, because they optimize for "it runs" rather than "it's hard to attack." For a closer look at what those tools leave out before launch, see Is Lovable Secure? What to Fix Before You Launch.

Where security headers fit in the bigger picture

Headers are a boundary at the edge. They don't replace what happens on the server or in the database. A well-hardened app has three layers:

Headers are the layer you can add in ten minutes, today, without touching your app logic. That's why they come first.

Frequently asked questions

What are the security headers?

Security headers are HTTP response headers that instruct the browser to behave safely: which resources it may load (Content-Security-Policy), whether to require HTTPS (Strict-Transport-Security), whether the page can be framed (X-Frame-Options), and whether to guess file types (X-Content-Type-Options). They mitigate XSS, clickjacking, MIME sniffing, and information leaks.

How do I find missing security headers?

Run your domain through securityheaders.com or Mozilla's HTTP Observatory, or run curl -I https://yoursite.com and look for the strict-transport-security, content-security-policy, x-content-type-options, and x-frame-options lines. Anything absent needs to be added.

Which security header is most important?

Content-Security-Policy. It's the only one that stops injected scripts from running, which is the core of XSS. HSTS is a close second because it's nearly free to add. X-Content-Type-Options and X-Frame-Options are one line each with no downside.

Do security headers make my site fully secure?

No. They harden the browser boundary but don't fix server-side problems like missing authorization, exposed secrets, or an API that returns too much data. They are one of several layers, so check the rest with a scanner that reads your site the way a stranger would.

Is X-XSS-Protection still needed?

No. It's deprecated and ignored by modern browsers. Older implementations could even be abused. OWASP no longer recommends it; Content-Security-Policy replaces it.

Sources


Security headers take ten minutes, but they only cover the surface. Rustine scans your AI-built app the way a stranger would — the request and what it returns — and hands you the fix to paste back into your agent. If you work inside Cursor or Claude Code, Rustine's MCP server can run the same check and fix findings on its own. Start with the free scan at https://rustine.io/en.

Is your app affected? Find out in a minute.Scan my site