ChillScan

← Everything ChillScan checks

Security Headers, Explained Chill

Security headers are little instructions your server sends with every page, telling the browser how careful to be. They take about ten minutes to add and close off a bunch of common attacks. Here's what each one does, a safe value to start with, and where to put it.

TL;DR

Add X-Content-Type-Options, Referrer-Policy, Permissions-Policy and X-Frame-Options today. They're low risk. Add HSTS once HTTPS is solid. Ship CSP in report-only mode first, then enforce it once the console is quiet.

See which headers your site is missing (free) →

1. Content-Security-Policy (CSP)

What it does: Tells the browser which places scripts, styles, images and connections are allowed to come from, so injected code has a much harder time running.

Safe starter value:

default-src 'self'; img-src 'self' data: https:; style-src 'self' 'unsafe-inline'; script-src 'self'; connect-src 'self' https:; frame-ancestors 'self'; base-uri 'self'; form-action 'self'

This is the one that can break things. Start in report-only mode (see below) before enforcing.

2. Strict-Transport-Security (HSTS)

What it does: Tells browsers to always use HTTPS for your site, even if someone types or clicks an http:// link.

Safe starter value:

max-age=31536000; includeSubDomains

Only add this once HTTPS works everywhere, including subdomains. Browsers remember it for the whole max-age. Some hosts (Vercel and Netlify, for example) already send HSTS for you; check what your custom domain actually returns before adding your own.

3. X-Frame-Options / frame-ancestors

What it does: Stops other sites from loading your pages inside an invisible iframe and tricking people into clicking things (clickjacking).

Safe starter value:

X-Frame-Options: SAMEORIGIN  (and/or CSP: frame-ancestors 'self')

frame-ancestors in CSP is the modern version; X-Frame-Options covers older browsers. Sending both is fine. If you intentionally embed your app elsewhere, list that origin in frame-ancestors.

4. X-Content-Type-Options

What it does: Stops the browser from guessing a file's type, so an uploaded “image” can't be run as a script.

Safe starter value:

nosniff

Basically zero risk. Just add it.

5. Referrer-Policy

What it does: Controls how much of your URL gets passed to other sites when people click links away from you.

Safe starter value:

strict-origin-when-cross-origin

Keeps tokens or IDs in your URLs from leaking to third parties, while analytics still see which site sent the visit.

6. Permissions-Policy

What it does: Switches off browser features (camera, mic, location, etc.) that your site doesn't use, so nothing embedded on your page can ask for them.

Safe starter value:

camera=(), microphone=(), geolocation=()

If your app actually uses the camera or location, change that entry to (self).

When a strict CSP breaks things (and how not to panic)

A strict CSP blocks anything you didn't list. That often includes analytics scripts, Stripe or other payment widgets, Google Fonts, embedded YouTube videos, Supabase/Firebase API calls, and inline scripts some frameworks inject. When it's wrong, pages look broken or buttons silently stop working.

The fix: start in report-only mode. Send the header as Content-Security-Policy-Report-Only instead of Content-Security-Policy. Nothing gets blocked; the browser just logs violations in the DevTools console. Click around your whole app, add the domains you actually need (e.g. script-src 'self' https://js.stripe.com), and once the console stays quiet, rename the header to enforce it.

Next.js and similar frameworks may need 'unsafe-inline' or nonces for scripts. That's okay as a first step; a CSP with a few compromises still beats no CSP.

How to set them on your host

Vercel (vercel.json)

Put this in vercel.json at your project root and redeploy. Next.js users can do the same via headers() in next.config.js.

// vercel.json
{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        { "key": "Strict-Transport-Security", "value": "max-age=31536000; includeSubDomains" },
        { "key": "X-Frame-Options", "value": "SAMEORIGIN" },
        { "key": "X-Content-Type-Options", "value": "nosniff" },
        { "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" },
        { "key": "Permissions-Policy", "value": "camera=(), microphone=(), geolocation=()" },
        { "key": "Content-Security-Policy-Report-Only", "value": "default-src 'self'; img-src 'self' data: https:; style-src 'self' 'unsafe-inline'; script-src 'self'; connect-src 'self' https:; frame-ancestors 'self'" }
      ]
    }
  ]
}

Netlify (_headers)

Create a _headers file that ends up in your publish directory (for Vite/React, put it in public/). A [[headers]] block in netlify.toml works too.

# public/_headers  (must end up in your publish folder)
/*
  Strict-Transport-Security: max-age=31536000; includeSubDomains
  X-Frame-Options: SAMEORIGIN
  X-Content-Type-Options: nosniff
  Referrer-Policy: strict-origin-when-cross-origin
  Permissions-Policy: camera=(), microphone=(), geolocation=()
  Content-Security-Policy-Report-Only: default-src 'self'; img-src 'self' data: https:; style-src 'self' 'unsafe-inline'; script-src 'self'; connect-src 'self' https:; frame-ancestors 'self'

Replit

Replit Deployments pass through whatever headers your app sends, so set them in your server code. Express shown here; Flask/FastAPI have equivalent middleware.

// Express example (server.js) — Replit serves whatever your app sends
app.use((req, res, next) => {
  res.setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
  res.setHeader("X-Frame-Options", "SAMEORIGIN");
  res.setHeader("X-Content-Type-Options", "nosniff");
  res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
  res.setHeader("Permissions-Policy", "camera=(), microphone=(), geolocation=()");
  res.setHeader("Content-Security-Policy-Report-Only",
    "default-src 'self'; img-src 'self' data: https:; style-src 'self' 'unsafe-inline'; script-src 'self'; connect-src 'self' https:; frame-ancestors 'self'");
  next();
});
// Or: npm i helmet  →  app.use(require("helmet")())

Lovable and custom-domain setups

If your hosting doesn't give you a headers config, you have a few options:

  • Export and host elsewhere: sync your Lovable project to GitHub and deploy it on Vercel or Netlify, then use the configs above.
  • Put Cloudflare in front: proxy your custom domain through Cloudflare and add headers with a Transform Rule (Rules → Transform Rules → Modify Response Header).
  • Meta tag fallback: CSP (but not the others, and not frame-ancestors) can go in <meta http-equiv="Content-Security-Policy"> in index.html. Better than nothing.

More Lovable-specific tips in the Lovable security checklist.

Check your work

Open DevTools → Network, click your page's main request, and look at Response Headers. Or skip the clicking and run a free ChillScan: it flags missing security headers, a CSP that allows inline scripts, and an HSTS max-age under six months. The free scan shows your top 3 issues; the Deep Scan lists all of them with fixes. Note that a report-only CSP still counts as “no Content-Security-Policy” until you switch it to enforcing, and browsers ignore frame-ancestors in report-only mode, so keep X-Frame-Options in the meantime.

Not sure what your site sends?

Paste your URL and ChillScan checks your headers along with leaked keys, HTTPS, SEO and speed. Free, no signup.

The full launch checklist →