pwnmyvibecode_

Security headers on Netlify

Create a file called _headers in your site's publish directory with a /* rule listing your security headers, or add a [[headers]] block to netlify.toml. For a Vite or React app, putting _headers in the public folder gets it copied into the publish directory on every build.

Updated

Check your live site now

Free, no signup, read only. A grade and plain fixes in seconds.

Where Netlify security headers go

Netlify reads custom headers from two places. Use one of them, not both, so there's only one copy of each header to maintain.

  • A plain text file named _headers (no extension) in the publish directory, the folder Netlify serves, such as dist or build.
  • A [[headers]] section in netlify.toml at the base of your repository, next to your build settings.

The most common mistake is putting _headers in the repository root when the site is published from dist. Netlify never sees it and nothing changes. With Vite, Create React App and most static site generators, files in public/ are copied into the build output unchanged, so public/_headers ends up in the right place. Apps built with Bolt are usually Vite projects deployed to Netlify, so the same applies.

The Netlify _headers file

The first line is the path the rule applies to; /* means every path. Each header follows on its own indented line as Name: value, with no quotes around the value.

public/_headers
/*
  Strict-Transport-Security: max-age=31536000; includeSubDomains
  Content-Security-Policy: default-src 'self' https: data: blob: 'unsafe-inline' 'unsafe-eval'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; form-action 'self'; upgrade-insecure-requests
  X-Frame-Options: SAMEORIGIN
  X-Content-Type-Options: nosniff
  Referrer-Policy: strict-origin-when-cross-origin
  Permissions-Policy: camera=(), microphone=(), geolocation=()

Commit the file and let Netlify build. The deploy summary lists how many header rules were processed, which is a quick way to confirm the file was found.

The same headers in netlify.toml

If you already keep build settings in netlify.toml, you can put the headers there instead. Values are TOML strings, so they need quotes:

netlify.toml
[[headers]]
  for = "/*"
  [headers.values]
    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 = "default-src 'self' https: data: blob: 'unsafe-inline' 'unsafe-eval'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; form-action 'self'; upgrade-insecure-requests"

Making the CSP strict on a Netlify static site

The CSP in both examples is a starter policy: it stops plugins, framing, base tag tricks and form posts to other sites, but it still lets inline scripts run, so it won't stop an injected script. On a static site the next step is to allow scripts only from your own domain. A production Vite build loads its code from files, so this usually works without changes to the app.

For a typical Vite app with a Supabase back end, a stricter policy looks like this. Add it as Content-Security-Policy-Report-Only first, use the site with the browser console open, and add any hosts you see blocked that are really yours:

public/_headers (add under /*)
  Content-Security-Policy-Report-Only: 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; object-src 'none'; base-uri 'self'; frame-ancestors 'self'

When the console stays clean, swap it in as the real Content-Security-Policy and delete the starter line. If you need per-request nonces (for example for inline scripts you can't move into files), Netlify Edge Functions can modify each response, but for most static apps moving the inline script into a file is simpler.

Netlify security header gotchas

  • Next.js on Netlify: set headers in next.config.ts like any other Next.js app, so they travel with the code.
  • Netlify Functions build their own responses. For API routes, set headers in the function's response as well, and check the result with curl rather than assuming the _headers rule reached it.
  • Netlify redirects http to https once the certificate for your custom domain is active. Keep HSTS anyway: the redirect protects the first visit, HSTS makes the browser skip http on every visit after that.
  • Keep every security header in the single /* rule. Spreading them across several path rules makes it easy to miss a page or end up with two different values.

How to confirm the headers on Netlify

terminal
curl -sI https://your-site.netlify.app | grep -i -E 'strict-transport|content-security|x-frame|x-content-type|referrer-policy|permissions-policy'

Every header from the file should come back once. If nothing comes back, open the deploy in Netlify and check that _headers appears in the deployed files; if it's missing, it wasn't in the publish directory.

Questions

Should I use _headers or netlify.toml?

Either works. _headers is simpler to read and edit; netlify.toml is handy if the rest of your Netlify config already lives there. Using both makes it easy to end up with conflicting values, so pick one.

Why isn't my Netlify _headers file working?

Almost always it's in the wrong folder. It has to be in the publish directory after the build. For Vite or React, put it in public/ so the build copies it into dist or build.

Does Netlify add security headers automatically?

Netlify provides https and the http to https redirect. CSP, frame protection, nosniff, Referrer-Policy and Permissions-Policy come from your own config. curl -sI on your site shows what is sent today.