Guide

Security headers in 10 minutes

Published · 6 min read

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-origin is 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-ancestors directive 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. includeSubDomains applies 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 sending max-age=0 over HTTPS and waiting for browsers to pick it up. Don’t add preload until 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:

  1. Ship the policy as Report-Only.
  2. Collect violations. MDN notes that reports are only sent if the policy includes a report-to directive, pointing at an endpoint you name in a Reporting-Endpoints header.
  3. Add each legitimate source to the policy, for example connect-src 'self' https://your-project.supabase.co.
  4. 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.

Free scan

See what Patch finds on your site.

Free, no signup. You get a score, every issue with its severity, and full detail on the top three.

No signup needed. Not legal advice; every check names its source.

More guides