Netlify serves your site over HTTPS, but out of the box it doesn't send most of the security headers that browsers use to block XSS, clickjacking and MIME sniffing. You can add them in two ways, a _headers file or netlify.toml, and neither needs a code change.
Option A: the _headers file
Create a plain-text file named _headers (no extension) that ends up in your publish directory. For Vite, CRA and most static generators, put it in public/ so it is copied into dist/ or build/.
/*
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=()
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'
The first line is the path pattern (/* means every page). Header lines underneath must be indented.
Option B: netlify.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=()"
Content-Security-Policy = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
netlify.toml lives in the repository root (or your base directory), not in the publish folder.
What these headers protect against
- HSTS forces HTTPS for future visits, so nobody on hotel Wi-Fi can downgrade the connection. Use
preloadonly when every subdomain is HTTPS. - X-Content-Type-Options stops the browser from running a file as a different type than you declared.
- X-Frame-Options and frame-ancestors block clickjacking through hidden iframes.
- Referrer-Policy keeps full URLs, including any tokens in query strings, from leaking to other sites.
- Permissions-Policy disables powerful browser APIs you don't use.
- Content-Security-Policy allowlists where scripts, styles, images and API calls may come from. It is your strongest defence against injected scripts.
Tuning CSP for common services
Add the origins your app actually uses:
- Supabase:
connect-src 'self' https://YOUR-PROJECT.supabase.co wss://YOUR-PROJECT.supabase.co - Firebase:
connect-src https://*.googleapis.com https://*.firebaseio.com wss://*.firebaseio.com - Stripe:
script-src https://js.stripe.com; frame-src https://js.stripe.com https://hooks.stripe.com - Google Fonts:
style-src https://fonts.googleapis.com; font-src https://fonts.gstatic.com - Netlify Forms post to your own domain, so
form-action 'self'covers them
To avoid breaking production, ship the policy as Content-Security-Policy-Report-Only first, browse the whole app with devtools open, fix the violations it reports, and then switch to the enforcing header. If your framework relies on inline scripts and you can't add nonces or hashes, you may need 'unsafe-inline' in script-src. That is weaker, but the other directives still help.
Different headers for different paths
/*
X-Frame-Options: DENY
/embed/*
X-Frame-Options: SAMEORIGIN
When several rules match, Netlify combines them. If the same header appears more than once, the values are merged into a comma-separated list rather than replaced. For security headers that rarely gives the result you want, so give each header exactly one definition per path.
Common gotchas
- _headers not applied: it isn't in the publish directory. Check the deploy's file browser in the Netlify UI.
- Functions missing headers: set them in the function's response, because file-based rules apply to static assets only.
- Framework plugins (Next.js on Netlify) may serve pages through functions. Use the framework's own header config or middleware for those routes.
Verify
curl -sI https://yoursite.netlify.app | grep -iE "strict-transport|content-security|x-frame|x-content-type|referrer|permissions"
Or paste your URL into ShipCheck for a free header and HTTPS check with fixes. On Vercel instead? See security headers on Vercel.