ShipWatchFree scan

Add security headers on Netlify, Vercel, and Cloudflare Pages (copy-paste)

Static hosts send almost none of the browser security headers by default; you add them with one small config file. Here are working blocks for each host, what every header does, and how to confirm they're live.

Short answer: On Netlify and Cloudflare Pages, put a _headers file in your publish directory (in a Vite app, public/_headers). On Vercel, add a headers array to vercel.json. Set HSTS, X-Content-Type-Options: nosniff, framing protection and a CSP, redeploy, and confirm with curl -I.

This page covers the whole header set and the file syntax per host. Writing the Content-Security-Policy itself for a Supabase + Stripe app has its own guide: a working CSP for Vite + React.

The headers, and what each one does

Netlify: _headers

Netlify reads a plain-text file named _headers from your publish directory. In a Vite project (Lovable and Bolt both use Vite), create public/_headers; Vite copies public/ into dist/ on build. A path line comes first, then indented headers.

/*
  Strict-Transport-Security: max-age=31536000; includeSubDomains
  X-Content-Type-Options: nosniff
  X-Frame-Options: DENY
  Content-Security-Policy: frame-ancestors 'none'
  Referrer-Policy: strict-origin-when-cross-origin
  Permissions-Policy: camera=(), microphone=(), geolocation=()

The CSP line above only covers framing, which is safe to ship today. Replace it with your full policy once you've tested one. Netlify also accepts the same rules as [[headers]] tables in netlify.toml. Either way, they don't apply to responses from Netlify Functions or edge functions; set headers in that code.

Vercel: vercel.json

Add a headers array to vercel.json in the project root. Each entry has a source pattern and a list of key/value pairs. "/(.*)" matches every path.

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

If you already have a vercel.json (for example with SPA rewrites), add headers next to it; don't replace the file. For a Next.js app (v0 output), you can set the same list in next.config.js instead.

Cloudflare Pages: _headers

Same format as Netlify: a _headers file with no extension in your build output, so public/_headers in a Vite app. The block above works unchanged. Cloudflare's limits: up to 100 rules, and 2,000 characters per line, which matters once your CSP grows. Headers in _headers aren't applied to Pages Functions responses.

Verify with curl

curl -sI https://yourapp.com | grep -iE "strict-transport|x-content-type|x-frame|content-security|referrer-policy|permissions-policy"
curl -sI http://yourapp.com | grep -iE "^HTTP|^location"

The first command should print one line per header. Run it on a deep link too, such as /settings. The second should show a 301 or 308 redirect to the https:// URL.

Add security headers for my host. For Netlify or Cloudflare Pages create public/_headers; for Vercel add a "headers" array to vercel.json without removing existing rewrites. Apply to all paths: Strict-Transport-Security: max-age=31536000; includeSubDomains, X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Content-Security-Policy: frame-ancestors 'none', Referrer-Policy: strict-origin-when-cross-origin, Permissions-Policy: camera=(), microphone=(), geolocation=().

Make it stick

What the free scan checks here (and what it doesn't)

The scan checks four of these headers: Content-Security-Policy (present or not), Strict-Transport-Security, X-Frame-Options or CSP frame-ancestors, and X-Content-Type-Options: nosniff. It also checks that the site uses HTTPS and redirects HTTP to HTTPS, and it reads your HTML and up to 25 linked scripts for leaked keys. It does not check Referrer-Policy or Permissions-Policy, and it doesn't grade CSP directives. Those are [manual] with curl.

FAQ

Which of these headers does the free scan check?
Four: Content-Security-Policy, Strict-Transport-Security, X-Frame-Options or CSP frame-ancestors, and X-Content-Type-Options: nosniff. It also checks HTTPS and the HTTP to HTTPS redirect. It doesn't check Referrer-Policy or Permissions-Policy; verify those with curl.
Why don't my _headers rules show up on API routes?
Netlify and Cloudflare Pages apply _headers only to static files they serve. Responses from Netlify Functions, edge functions or Pages Functions must set their own headers in code.
Is X-Frame-Options still needed if I set frame-ancestors?
Modern browsers follow CSP frame-ancestors, which is the more flexible option. Sending X-Frame-Options: DENY as well costs nothing and covers older browsers, so the blocks on this page send both.

Scan your app free in 30 seconds

Paste your live URL for a plain-English report with fix prompts. If you keep shipping, weekly monitoring re-checks it: Solo $19/mo or Pro $49/mo.

Scan my app free