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
- Strict-Transport-Security (HSTS): tells the browser to use HTTPS for your domain for
max-ageseconds, so later visits can't be downgraded to plain HTTP. Only addincludeSubDomainsif every subdomain serves HTTPS. - X-Content-Type-Options: nosniff: stops the browser guessing a file's type, so an uploaded text file can't be run as a script.
- X-Frame-Options: DENY and CSP frame-ancestors 'none': stop other sites from loading your app in a hidden iframe (clickjacking).
- Content-Security-Policy: limits where scripts load from and where the page may connect. The biggest XSS defence, and the one most likely to break things, so roll it out report-only first.
- Referrer-Policy [manual]:
strict-origin-when-cross-originsends only your origin, not full URLs with tokens or IDs, to other sites. - Permissions-Policy [manual]: turns off browser features you don't use, for example
camera=(), microphone=(), geolocation=().
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
- Commit the config file. Headers set only in a dashboard get lost when you move hosts.
- Re-run the curl check after changing hosts, adding a custom domain, or moving routes into functions.
- Start HSTS with a short
max-age(say 300) if you're unsure every subdomain has HTTPS, then raise it. - Headers are one item of a launch check. The rest is in the vibe coding security checklist and the 15-minute pre-launch pass.
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.