Security headers are short instructions your web host sends with every page. They tell the browser things like “only ever load this site over HTTPS” or “don’t let other sites put me in a frame”. Visitors never see them. They cost nothing. Plenty of AI-built sites ship without them, because headers live in your hosting settings rather than in the app code an AI tool writes.
This guide gives you a sensible starter set, what each header does in one line, and copy-paste setups for Netlify, Vercel and Cloudflare Pages. Ten minutes is realistic for everything except the Content Security Policy, which deserves a bit longer.
What each header does
| Header | What it does, in one line |
|---|---|
| Strict-Transport-Security (HSTS) | Tells browsers to use HTTPS only, and to upgrade any future HTTP attempts automatically. |
| X-Content-Type-Options | With nosniff, stops the browser guessing a file’s type instead of trusting the declared one. |
| Referrer-Policy | Controls how much of your page’s address is passed on when someone clicks a link away from it. |
| Permissions-Policy | Switches off browser features you don’t use, such as the camera, microphone or location. |
| X-Frame-Options | Says whether other sites may show your page inside a frame, which helps against clickjacking. |
| Content-Security-Policy (CSP) | Lists which sources the page may load scripts, styles, images and other resources from. |
A few notes from the official docs:
- Referrer-Policy: MDN says
strict-origin-when-cross-originis already the browser default. The OWASP headers cheat sheet recommends setting it explicitly anyway, rather than relying on defaults. - Permissions-Policy: MDN marks it as experimental and not yet supported in every major browser, so treat it as a bonus rather than a lock.
- Framing: CSP has a
frame-ancestorsdirective that does the same job as X-Frame-Options with more options. OWASP suggests using it where you can. The starter set below sends both.
Skip X-XSS-Protection
You will see X-XSS-Protection: 1; mode=block in a lot of old tutorials, and even in some current platform examples. MDN marks it as deprecated and non-standard, and warns that it “can create XSS vulnerabilities in otherwise safe websites”. MDN recommends using Content-Security-Policy instead. OWASP’s advice is not to set it, or to switch it off explicitly. Leave it out.
The starter set
Here is a set that suits most small sites. It works in almost any setup because it doesn’t depend on what your page loads.
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
X-Frame-Options: DENY
Content-Security-Policy: frame-ancestors 'none'
Content-Security-Policy-Report-Only: default-src 'self'; frame-ancestors 'none'; form-action 'self'
Two things to adjust before you paste:
- HSTS and subdomains.
includeSubDomainsapplies the rule to every subdomain too. Leave it off if any subdomain still serves plain HTTP. MDN notes browsers ignore HSTS sent over plain HTTP, and that turning it off later means sendingmax-age=0over HTTPS and waiting for browsers to pick it up. Don’t addpreloaduntil you’ve read what it commits you to. - Permissions-Policy. If your app genuinely uses the camera or location, remove that item from the list.
The last two lines are deliberate. One is an enforced CSP that only blocks framing, which is safe for almost any site unless you want it embedded elsewhere. The other is a fuller policy in report-only mode. It blocks nothing, and it reports nothing either until you give it somewhere to send reports, which we’ll come to.
A strict CSP can break your site
A Content Security Policy is the most useful header here and the easiest to get wrong. A policy like default-src 'self' says “only load things from my own domain”. That sounds tidy until you remember your analytics, your fonts, your payment form, your chat widget and your Supabase project all live somewhere else. Enforce that policy blind and they stop working.
Patch once watched a site deploy a perfect CSP. It was very secure. It also no longer had a checkout.
So start with Content-Security-Policy-Report-Only. MDN describes it as a way to monitor violations “without enforcing the security policies”. The OWASP CSP cheat sheet describes it as the usual step before enforcing, and Vercel’s CSP guide gives the same advice. A sensible order:
- Ship the policy as Report-Only.
- Collect violations. MDN notes that reports are only sent if the policy includes a
report-todirective, pointing at an endpoint you name in aReporting-Endpointsheader. - Add each legitimate source to the policy, for example
connect-src 'self' https://your-project.supabase.co. - When nothing legitimate is being flagged, rename the header to
Content-Security-Policy.
You can send both headers at once. The MDN CSP guide says the enforced one is applied while the report-only one only generates reports.
Netlify: _headers
Create a plain text file called _headers in your publish directory. For a Vite app that means putting it in public/: the Vite docs say files there are copied to the root of the build output as-is. The Netlify headers docs use this format: a path, then indented headers.
/*
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
X-Frame-Options: DENY
Content-Security-Policy: frame-ancestors 'none'
Content-Security-Policy-Report-Only: default-src 'self'; frame-ancestors 'none'; form-action 'self'
Netlify’s docs note that these headers only apply to files Netlify serves directly. Responses from Functions, Edge Functions (including server-rendered pages) and proxied content must set their own headers.
Vercel: vercel.json
Add a headers array to vercel.json at the root of your project. The vercel.json reference uses "source": "/(.*)" to match every path.
{
"headers": [
{
"source": "/(.*)",
"headers": [
{ "key": "Strict-Transport-Security", "value": "max-age=31536000; includeSubDomains" },
{ "key": "X-Content-Type-Options", "value": "nosniff" },
{ "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" },
{ "key": "Permissions-Policy", "value": "camera=(), microphone=(), geolocation=()" },
{ "key": "X-Frame-Options", "value": "DENY" },
{ "key": "Content-Security-Policy", "value": "frame-ancestors 'none'" },
{ "key": "Content-Security-Policy-Report-Only", "value": "default-src 'self'; frame-ancestors 'none'; form-action 'self'" }
]
}
]
}
The Vercel reference example includes X-XSS-Protection. You can leave that one out, for the reasons above.
Cloudflare Pages: _headers
Cloudflare Pages uses the same _headers format as the Netlify example, so the same file works. The Cloudflare Pages headers docs say to put it in your static asset folder (usually public/ with a framework) so it ends up in the build output. Two limits worth knowing: up to 100 header rules, and 2,000 characters per line. As with Netlify, the file doesn’t apply to responses from Pages Functions, so server-rendered pages need headers set in code.
If your host won’t let you set headers
Some app builders host your site for you and don’t document a way to set custom headers. At the time of writing, Lovable’s hosting docs say HTTPS is included automatically but don’t describe custom headers. Check your platform’s docs or ask its support.
There is a partial fallback for CSP. The MDN CSP guide says a policy can go in a <meta http-equiv="Content-Security-Policy"> tag, and calls that useful for client-side-rendered apps with only static resources. It doesn’t support every feature, though. MDN notes that frame-ancestors and report-only policies don’t work in a meta tag, and the other headers in this guide need the real thing.
How to test
- Browser devtools. Open your site, open devtools, go to the Network tab and reload. Click the first request (your page) and look at Response Headers. The Chrome DevTools reference walks through it.
- Click around. With an enforced CSP, test every page that loads something from elsewhere: sign-in, payments, embedded video, forms.
- ShipPatrol. Our security headers check reads the headers on your live site, tells you which are missing, and flags weak values such as a CSP that allows any script. It runs as part of every free scan.
For background on the project behind much of this advice, see the OWASP Secure Headers Project. If your site uses Supabase, Supabase row-level security, explained covers the part headers can’t fix.
Check your site
Run a free scan to see which headers your site sends today. It takes less time than reading this paragraph twice.