Security Headers Every Web App Needs
You deployed your app. It works. Users are signing up. But open your browser's DevTools, check the response headers, and you'll probably see... nothing.
Missing security headers are the most common finding in our scans. They're also the easiest to fix — usually a single config file.
Why Headers Matter
Security headers tell the browser how to behave. Without them:
- Your app can be embedded in malicious iframes (clickjacking)
- Scripts from any domain can run on your page (XSS attacks)
- Browsers will send credentials to any site
- Users can be downgraded from HTTPS to HTTP
All major vulnerability scanners flag missing headers. And if an enterprise customer ever runs a pentest on your app, this is the first thing they'll find.
The 6 Essential Headers
1. Content-Security-Policy (CSP)
What it does: Controls which sources can load scripts, styles, images, and other resources on your page.
Why it matters: Prevents XSS attacks by blocking inline scripts and unauthorized external scripts.
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://*.supabase.co
Note for vibe coders: If you use Supabase, add your project URL to
connect-src. If you use Google Fonts, addhttps://fonts.googleapis.comtostyle-srcandhttps://fonts.gstatic.comtofont-src.
2. Strict-Transport-Security (HSTS)
What it does: Forces browsers to always use HTTPS, even if the user types http://.
Why it matters: Prevents SSL stripping attacks and protects users on public WiFi.
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
3. X-Frame-Options
What it does: Prevents your app from being embedded in an iframe.
Why it matters: Stops clickjacking attacks where an attacker overlays an invisible frame of your app on a malicious page.
X-Frame-Options: DENY
4. X-Content-Type-Options
What it does: Prevents MIME-type sniffing.
Why it matters: Stops browsers from interpreting files as a different MIME type, which can lead to XSS via uploaded files.
X-Content-Type-Options: nosniff
5. Referrer-Policy
What it does: Controls how much URL information is shared when navigating away from your site.
Why it matters: Prevents sensitive URL parameters (tokens, user IDs) from leaking to third-party sites.
Referrer-Policy: strict-origin-when-cross-origin
6. Permissions-Policy
What it does: Controls which browser features (camera, microphone, geolocation) your app can use.
Why it matters: Prevents malicious scripts from accessing sensitive device features.
Permissions-Policy: camera=(), microphone=(), geolocation=()
Copy-Paste Configs
Vercel (vercel.json)
{
"headers": [
{
"source": "/(.*)",
"headers": [
{
"key": "Content-Security-Policy",
"value": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://*.supabase.co"
},
{
"key": "Strict-Transport-Security",
"value": "max-age=31536000; includeSubDomains; preload"
},
{
"key": "X-Frame-Options",
"value": "DENY"
},
{
"key": "X-Content-Type-Options",
"value": "nosniff"
},
{
"key": "Referrer-Policy",
"value": "strict-origin-when-cross-origin"
},
{
"key": "Permissions-Policy",
"value": "camera=(), microphone=(), geolocation=()"
}
]
}
]
}
Netlify (_headers)
/*
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://*.supabase.co
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
Cloudflare Pages (_headers)
Same format as Netlify:
/*
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://*.supabase.co
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
Next.js (next.config.js)
/** @type {import('next').NextConfig} */
const nextConfig = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'Content-Security-Policy',
value: "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://*.supabase.co",
},
{
key: 'Strict-Transport-Security',
value: 'max-age=31536000; includeSubDomains; preload',
},
{
key: 'X-Frame-Options',
value: 'DENY',
},
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin',
},
{
key: 'Permissions-Policy',
value: 'camera=(), microphone=(), geolocation=()',
},
],
},
];
},
};
module.exports = nextConfig;
Testing Your Headers
Quick Test
Open DevTools → Network tab → Click your page request → Check the Response Headers section.
Automated Test
Use Proveably to scan your app — we check all 6 headers and flag any that are missing or misconfigured, with specific fix instructions for your hosting platform.
Common Gotchas
- CSP breaks inline styles. Most UI libraries use inline styles. Start with
'unsafe-inline'forstyle-srcand tighten later. - CSP blocks Supabase. Add
https://*.supabase.cotoconnect-srcor your API calls will fail silently. - X-Frame-Options blocks your own embeds. If you need to iframe your app somewhere, use
SAMEORIGINinstead ofDENY. - Vercel/Netlify already set HSTS. They do — but adding your own header ensures consistency and adds
preloadsupport.
Check Your Headers Now
Your headers take 5 minutes to fix. Scan your app to see exactly which ones you're missing:
Related reading: