Security headers are instructions your server sends with every page, telling the browser what it may and may not do with your site. They do not fix a vulnerability on their own. What they do is make whole classes of attack much harder: injected scripts that cannot run, a login page that cannot be framed by a fake site, a connection that cannot be downgraded to plain HTTP.
They are also among the most commonly missing basics. VibeEval's August 2026 scan of 30,998 live vibe-coded apps found 99% had at least one finding, mostly missing security headers.
Short answer: add
Strict-Transport-Security,Content-Security-Policy(start in report-only mode),X-Content-Type-Options: nosniff,frame-ancestors 'none'orX-Frame-Options: DENY, and aReferrer-Policy. Never allow any origin to make credentialed CORS requests.
The headers that matter most
| Header | Protects against | A sensible starting value |
|---|---|---|
Strict-Transport-Security |
Connections downgraded to plain HTTP | max-age=31536000; includeSubDomains |
Content-Security-Policy |
Injected scripts (XSS) running in your page | See below; start with Content-Security-Policy-Report-Only |
X-Content-Type-Options |
Browsers guessing a file is a script | nosniff |
X-Frame-Options / CSP frame-ancestors |
Your pages framed by another site (clickjacking) | DENY / frame-ancestors 'none' |
Referrer-Policy |
Full URLs (with tokens or ids) leaking to other sites | strict-origin-when-cross-origin |
Permissions-Policy |
Pages silently using camera, microphone or location | camera=(), microphone=(), geolocation=() |
Content Security Policy without breaking your app
CSP is the most powerful header and the easiest to get wrong. It lists where scripts, styles, images and connections may come from. A typical single-page app on Supabase might start here:
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://YOUR-PROJECT.supabase.co wss://YOUR-PROJECT.supabase.co;
frame-ancestors 'none';
base-uri 'none';
form-action 'self'
Add every service your app really talks to (analytics, payments, fonts) to the matching directive. Then:
- Ship it first as
Content-Security-Policy-Report-Only. The browser reports violations in the console but blocks nothing. - Use your app for a day, including sign-in, payments and uploads. Add anything legitimate that shows up.
- Switch the header name to
Content-Security-Policyto enforce it.
Avoid 'unsafe-eval' and 'unsafe-inline' in script-src where you can; they remove most of the protection CSP offers.
CORS: the one setting to never get wrong
CORS decides which other websites may read responses from your API in a visitor's browser. The dangerous pattern is an API that reflects whatever Origin it receives and also sends Access-Control-Allow-Credentials: true. That lets any website make signed-in requests on your users' behalf and read the answers.
Allow only the origins you own, by exact match. For a public, read-only API with no cookies, Access-Control-Allow-Origin: * without credentials is fine.
Copy-paste configs
Vercel (vercel.json):
{
"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": "Referrer-Policy", "value": "strict-origin-when-cross-origin" },
{ "key": "Permissions-Policy", "value": "camera=(), microphone=(), geolocation=()" }
]
}
]
}
Netlify and Cloudflare Pages (a _headers file in your published folder, for example public/_headers):
/*
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
Add your Content-Security-Policy-Report-Only line to the same place once you have written it.
How to check your headers
In your browser's developer tools, open Network, reload, click the first document request and read Response Headers. Or from a terminal:
curl -sI https://your-app.com | grep -iE "strict-transport|content-security|x-frame|x-content-type|referrer-policy|access-control"
A PreflightX scan checks enforced CSP, HSTS, framing protection and wildcard CORS, and folds them into your defense score.
Frequently asked questions
Will adding security headers break my app?
HSTS, nosniff, framing protection and a referrer policy rarely break anything. CSP can, which is why you start it in report-only mode.
Should I add preload to HSTS?
Only once you are sure every subdomain works over HTTPS, and you understand that removing it from browser preload lists takes months.
Are missing headers a vulnerability? They are missing defenses, not a confirmed exploit. They matter most when something else goes wrong, for example a script injection that a good CSP would have blocked.
Check what your app exposes right now
PreflightX runs a read-only scan of your live app's public surface: exposed keys in your JavaScript, readable Supabase tables, open storage buckets, deployed .env and .git files, source maps and missing security headers. Every finding is free to see.