StackAttestTechnical Trust
Security headers

What a security headers check tells you, and what it does not

This is the cheapest result on the site to fix and the easiest one to overrate. Four of the five headers are a line of configuration each and you could set them this afternoon. The fifth is genuine work and worth it. None of them stops somebody who can already read another customer's records by asking your server for them politely.

Run the free checkSee pricingSee an example Passport

How this control gets settled

Headers are the one subject on this site that a stranger holding your address can establish completely. What came back on the response is the entire answer, so the layer that fetches a response is the layer that settles it, and reading your source is there to explain the result rather than to produce it.

Public URL scan

E4
URL scan

Fetches the address and reads the headers off the response: strict transport security, a content security policy, nosniff, a frame-ancestors directive or an X-Frame-Options value, and a referrer policy. This is the layer that settles this control, and it needs nothing but the address. The grade records how a result was established rather than how much it covers, so a header read off the running deployment is runtime verified. Its limit is reach, and what it cannot reach it reports as uncovered.

Runtime validation

E4
Runtime

Builds and runs the application and reads the same headers off a response from the deployment it produced, rather than from whatever sits in front of your production site. That separates a header your application sets from a header something else adds on the way out, and that distinction is what decides where a missing one has to be fixed.

Repository validation

E2
Repository

Reads where your application sets headers of its own. It cannot see what a proxy, a platform or a cache adds or strips afterwards, so on this control it explains the result rather than producing it: the response is the fact, and the source is the reason.

The five we test, what each prevents, and where the list stops

One header per entry, with the attack it interrupts. If you read only one, read the content security policy: it is the only one here that takes a week rather than a minute, and it is the only one that carries that much weight.

Strict-Transport-Security: the request you lose is the first one

Encryption protects a connection that is already https. The gap is the request before it: somebody types your domain, the browser tries plain http, and whoever sits on that network gets to answer first. Redirecting closes it for every visit after the first one. This header closes the first one, by telling the browser to refuse plain http for your domain for a stated period and to remember that. Start with a few months, add subdomains only once every subdomain is https, and treat the period as a promise you cannot shorten in a hurry.

Content-Security-Policy: the only one here that is real work

It tells the browser which sources of script, style, frame and connection are legitimate for your pages, so script that arrives any other way does not execute. That interrupts an entire class of injected script rather than one instance of it, which is why it sits at the top of the list. It is also the one that breaks your own site while you write it, because a real application loads from more places than anybody remembers. Ship it in report-only mode first, read what it would have blocked, then enforce. Keep unsafe-inline out of it: a policy that allows inline script has cost you the effort and kept almost none of the protection.

Clickjacking protection: frame-ancestors, with X-Frame-Options behind it

Without it, anyone can load your application inside an invisible frame on a page of their own and collect the clicks a visitor believes they are giving to something else. Two headers do this job. X-Frame-Options is the older one and is understood everywhere. The frame-ancestors directive inside a content security policy is the current one, browsers prefer it, and it can name which parents are allowed rather than choosing between all and none. Setting both is normal, and the check counts either as this protection being present.

X-Content-Type-Options: stop the browser guessing

Browsers used to inspect a response and decide for themselves what it really was, which is how a file uploaded as an image ends up executed as script because it looked enough like one. The value nosniff turns that off, so a response is treated as the type you declared it to be. It is one line, there is nothing in it to configure wrongly, and its only real cost is that your declared content types now have to be right.

Referrer-Policy: stop handing out your own URLs

By default a browser tells the next site where the visitor came from, and your addresses say more than you think: a reset token, a share link, an invitation, an internal identifier, a path that names a customer. A policy of strict-origin-when-cross-origin keeps whole paths inside your own site and sends only your origin outward. It is the smallest of the five and it is the one quietly giving away working links. One honest note on it: this is the header that is read and reported rather than graded, because the pass criteria for the control turn on the other four.

A full set is a floor, not a defence

Each of these narrows what a browser will do with your pages. Not one of them decides whether your server hands one customer's record to another customer who asked for it, whether a privileged credential is sitting in your bundle, or whether an endpoint accepts whatever it is sent. An application can carry all five, score perfectly on any scanner, and be emptied through a route that never checks who is calling. Set them, because they are cheap and they close real attacks. Then go and look at authorisation, which is where the damage lives.

Where to set them, and how to know you did

Which file you edit is decided by where your headers come from today, and most of the wasted afternoons on this subject start with editing the wrong one.

  1. Find out where your headers come from now

    Fetch your own site, read the response headers, and compare them with what your application sets. A header on the response that is nowhere in your code is being added by your platform, proxy or CDN, and that is where it will have to change. A header your code sets that never arrives is being stripped or overwritten on the way out. Knowing which of those you have is the difference between a five minute fix and an afternoon spent in the wrong file.

  2. Set the four cheap ones wherever every response passes

    Strict transport security, nosniff, a referrer policy and clickjacking protection are static values with no per-page thinking behind them. At an edge they are one transform rule, in a proxy they are four added lines, in an application they are one middleware. If you have an edge, prefer it: it covers responses your framework never generates, including platform error pages and static assets.

  3. Write the content security policy in report-only first

    Deploy it as report-only, use the application normally for a few days, and read what it would have blocked. That list is an inventory of what your pages genuinely load, which nobody has ever written down and everybody guesses at. Tighten it against that, and only then enforce it.

  4. Read a second response, not just the home page

    Headers set in application middleware routinely miss the responses that never reach it: a 404 from the platform, a cached asset, a maintenance page. Request a path that does not exist and read the headers on that answer too. A policy on your home page and nothing on your error page is a real gap, and one fetch will not show it to you.

  5. Check again after anything that changes routing

    Moving host, adding a proxy, or putting a cache in front of the application are the three changes that drop a header without a word. Re-reading the response takes half a minute and it is the only thing that catches them.

What the result looks like

An illustrative extract, not a real customer result. Every row carries the evidence grade behind it, and a control nobody could establish says so.

ControlStatusEvidenceNote
Security response headers are presentPartialE4Strict transport security, nosniff and a referrer policy on the response, no content security policy
Transport security (TLS) is enforcedVerifiedE4Certificate verified against the running deployment
Error responses do not leak internalsFailedE4A path that does not exist returned a framework stack trace
Object ownership is enforced server-sideUncoveredNot gradedOne anonymous view of a site cannot answer a question about two accounts
Secrets are managed outside source codeUncoveredNot gradedThat needs the repository, and the layer that ran did not read one

Mapped to published standards

Each control is tested against a named clause, so a result means something outside our own vocabulary. Mapping is not certification, and none of these bodies endorse StackAttest.

OWASP ASVS 5NIST SSDFCWE

What this check is not

  • Headers are a floor. A perfect set says nothing about who your server will answer, what it returns to a caller asking for somebody else's record, or what is committed in your source. A clean header result is one control out of thirteen settled, which is what the report calls it.
  • It reports the headers on the responses it fetched. A header present on your home page and missing from an error page or an API route is a genuine gap that a check reading one response has not seen, so set them where every response passes and read more than one when you verify.
  • A content security policy is judged present rather than correct. Whether a policy is tight enough for your application is a judgement about your application: one that allows inline script is present and does very little, and no scanner is in a position to decide that for you.
  • It is not a penetration test. A deterministic check and a runtime probe are not an adversarial human engagement, and there are classes of weakness only a person hunting for them will find.
  • It is not SOC 2 and does not replace it. SOC 2 attests to organisational controls over time; this validates technical controls in the software. They answer different questions for different buyers.
  • It does not replace human due diligence. It gives a reviewer repeatable evidence to start from, which is a different thing from being the reviewer.
  • Controls the run could not reach are reported as uncovered rather than passed. A result you did not earn is worse than no result.

Questions

What is a security headers check?

A request to your address and a read of the headers that came back, against a named list. Ours is five: strict transport security, a content security policy, nosniff, clickjacking protection through frame-ancestors or X-Frame-Options, and a referrer policy. Because the answer is read off the running deployment rather than inferred from your code, the result is runtime verified evidence about the site as it is actually served.

Which security header matters most?

The content security policy, by a distance, and it is also the only one that will cost you a week. It restricts where script and other resources may come from, which interrupts a whole class of injected script rather than one example of it. The other four are worth doing this afternoon because they are a line each, not because any of them carries comparable weight.

Should headers be set at the edge or in the application?

Wherever every response passes. An edge or proxy covers answers your application never generates, including platform error pages and static assets, and it is usually one rule for the whole site. Application middleware knows about the request, which matters for a policy that uses a per-response nonce. What to avoid is setting them in both places without deciding which wins, because that answer differs by header and by proxy.

Is X-Frame-Options obsolete?

Superseded rather than obsolete. The frame-ancestors directive in a content security policy is the current mechanism and it is more precise, because it can name the parents you allow. Older browsers and some intermediaries still read X-Frame-Options, and keeping both costs nothing. Our check counts either one as this protection being present.

A scanner gave my site an A. Am I secure?

You have a good result on one control. A header grade describes what a browser will do with your pages, and the incidents that hurt software like yours happen on the server: a record returned to the wrong account, a privileged credential in a bundle, an endpoint that accepts anything it is sent. A site with a perfect header score can be emptied by one unauthenticated request to the right path.

Does the free check test these?

Yes, and this is the control it reports most often, because it needs nothing but your address. Paste the URL and you get what is present and what is missing from the five, alongside transport security, cookie flags and error handling, with the controls a public address cannot reach named as uncovered rather than passed.

Related

Free AI app security checkAPI rate limitingExposed API keysRow level security explainedBroken access control testAI app security auditProduction readiness audit

Find out what is actually true of your application

Start with the layer you can run today. The report says which layers ran and what they could not reach, so the result is honest about its own limits.

Run the free checkSee pricing