Security headers are instructions your server sends along with the page. They don't hide a database, and they don't rotate a key. They tell the browser how to treat the page it just received. Missing ones are worth fixing. They're also a smaller deal than a secret key in the JavaScript. Here I'll explain the headers Deploy Safely looks at, in the words the browser vendors' documentation uses, and then tell you exactly when the free check bothers to mention them.
You can see the headers yourself. Open the published site, open developer tools, choose the network panel, reload, and click the document request (the first one, the HTML). The response headers are a list of names and values. You're reading what your host actually sent, which can differ from what a config file says if the host never applied it.
Content-Security-Policy
MDN's CSP guide describes Content-Security-Policy as a set of rules for where the page may load things from, and for which pages may embed it. A policy is a header value full of directives, like script-src and frame-ancestors. It isn't a single on/off switch. A missing policy means you didn't give the browser those extra rules. It doesn't, by itself, mean the site has a cross-site scripting bug. It means one layer you could have added is absent.
The directive that lines up with "don't let another site put my page in a frame" is frame-ancestors. MDN says frame-ancestors 'none' means the page may not be embedded, and that this is similar to X-Frame-Options: DENY. The quotes around none are required. MDN also notes that frame-ancestors doesn't fall back to default-src. A policy that says default-src 'none' and never mentions frame-ancestors still allows embedding. If you add a CSP and forget that directive, you haven't blocked frames.
CSP can also break a page. If you set script-src 'self' and the app loads a script from somewhere else, the browser will refuse that script. Start with a policy you have loaded in a browser, on the real pages, including checkout. A policy copied from a blog and never tested is how a launch goes blank. Aim for specific. A longer policy isn't a better one.
X-Frame-Options
X-Frame-Options is the older header for the same framing question. DENY means the document can't be framed. SAMEORIGIN means it can be framed only by a page from the same origin. MDN says ALLOW-FROM is obsolete and that modern browsers ignore the header if they see it. Use frame-ancestors for anything more specific than deny or same-origin, and you can send both headers so older browsers still have an answer.
MDN's note if you send neither, and you also have no other embedding restriction: the browser will allow other sites to embed the document. That's the clickjacking case people mean. A visitor thinks they're clicking your button, and they're clicking a control on your page inside someone else's frame. For a marketing page this is often a low practical risk. For a page that changes data or pays someone, it's worth closing. DENY is the right value unless you truly need your own app to frame the page.
Strict-Transport-Security
HSTS is the Strict-Transport-Security header. MDN says it tells the browser that the host should only be reached over HTTPS, and that later HTTP attempts should be upgraded to HTTPS. It also says the browser won't let the person click through a certificate error for that host while the policy is remembered. The header includes max-age in seconds. includeSubDomains extends the rule to subdomains. preload is a separate submission process, and MDN says that using it requires a max-age of at least a year and includeSubDomains.
Two details keep people from fooling themselves. MDN says browsers ignore the header if it arrives over plain HTTP, so a site that isn't on HTTPS yet can't "turn on HSTS" and become safe. And the header doesn't change the current response. It changes how the browser treats later visits. The first visit still needs a normal HTTPS redirect. Send a permanent redirect from HTTP to HTTPS, and send HSTS only on the HTTPS response.
Don't set a huge max-age on a host you aren't sure will keep a valid certificate, especially with includeSubDomains, because you're asking browsers to remember the rule. A year is the figure MDN ties to preload. You can start shorter while you confirm every subdomain serves HTTPS.
X-Content-Type-Options and Referrer-Policy
X-Content-Type-Options has one directive you will see: nosniff. MDN says it tells the browser to respect the Content-Type you sent and not sniff a different type. For scripts and styles, a mismatch can be blocked. For other responses, the browser uses the declared type instead of guessing. Set the real content type on your files, and send nosniff. This one is a single line, with no policy language to learn.
Referrer-Policy controls how much of the current URL is sent in the Referer header when the browser asks for the next thing. MDN says strict-origin-when-cross-origin is the default when no policy is specified: full URL for same-origin requests, origin only for cross-origin HTTPS, and nothing when going from HTTPS to HTTP. Sending the header anyway makes that choice explicit, including for a browser that might otherwise use a different fallback. unsafe-url is the value MDN warns about, because it sends the full URL even to less safe destinations. You don't need that. If your URLs contain reset tokens or email addresses in the query string, a tighter policy (or, better, not putting secrets in URLs) matters more than the default.
What the free check actually reports
Deploy Safely reads the headers on the main page response. It doesn't grade every image, font, or script response separately. It watches five things:
Content-Security-Policyis present.Strict-Transport-Securityis present, and only when the page itself was loaded over HTTPS.X-Content-Type-Optionsis present.- Either
X-Frame-Optionsor aContent-Security-Policythat mentionsframe-ancestors. Referrer-Policyis present.
The report mentions missing headers only when two or more of those are absent. One missing header doesn't create the finding. The finding is low severity. The product copy treats it as less urgent than a leaked key, which matches what these headers do: they're instructions for the browser, and the lock on your database lives somewhere else.
That design comes with a few honest limits. The check doesn't parse whether your CSP is strict. A CSP of default-src * would count as present. The check doesn't know whether HSTS max-age is long enough, and it doesn't look at preload lists. It doesn't fail you for skipping Referrer-Policy alone, and MDN says browsers already default to strict-origin-when-cross-origin. It doesn't look at headers on a second domain that serves your assets. And it never claims that present headers mean the app is safe. The same check is still looking for leaked keys, /.env, /.git, and, when the page already contains the config, Supabase or Firebase data. Those are the higher-severity items. Headers are the note at the end.
If you want the hand version, the network panel plus the checklist covers it. Builder-specific pages: Cursor, v0, and Lovable. Hosts differ. On Vercel or a Next.js app, headers often live in next.config or vercel.json. On a static host, they live in that host's header settings. The browser doesn't care which file you used. It cares what the response contains after you publish.
A prompt you can paste
Paste this into the builder, publish again, and run the check a second time. If a secret was already on the live site, rotate it in that vendor's dashboard. The prompt cannot do that part.
Add browser safety headers on the public site without breaking the pages. Do not weaken the app to satisfy a scanner. 1. On HTML responses, send Content-Security-Policy, X-Content-Type-Options: nosniff, X-Frame-Options: DENY, and Referrer-Policy: strict-origin-when-cross-origin. 2. Include frame-ancestors 'none' in the Content-Security-Policy, unless this app is embedded on purpose. If it is, name the parent site instead of removing the directive. 3. On HTTPS responses, send Strict-Transport-Security with a max-age. Do not add includeSubDomains until every subdomain serves HTTPS. 4. Keep the Content-Security-Policy strict enough to load this app. Allow the scripts and connections the app really uses. Do not set default-src * to make the header exist. 5. Tell me the file you changed. I will publish and confirm the headers on the document request in the browser. Do not remove a header because a third-party widget complained until we know which directive it needs.