📋 HTTP security header checks explained

Every response-header check of the headers module: what is tested, why it matters and how it is scored – from HSTS and CSP quality down to cookie flags and information leaks. Check your domain →

Last updated: September 2026 · 34 checks documented

Website configured (A/AAAA records) #

The Website and HTTP security headers modules check a running website. If neither the domain itself nor www has A or AAAA records, there is no website to connect to – a perfectly normal setup for domains that are used for e-mail only.

RatingOnly appears when the domain itself exists (NOERROR) but has no A/AAAA records, www has none either (conclusive NOERROR/NXDOMAIN answers) and no CNAME is configured on either name: skip; the module is reported as not applicable instead of being graded F. Everything else – a DNS failure (e.g. a broken DNSSEC chain), a nonexistent domain or a dangling CNAME – keeps the regular checks, so a broken site still shows up as unreachable. Before the claim is made, the name is also asked for an HTTPS record: RFC 9460 lets a name reach its service through an AliasMode record with no address records of its own. If one is found, the module reports skip and not measured and is NOT declared not applicable – the site exists, it simply was not tested here; if that lookup does not answer conclusively, nothing is claimed in either direction.
How to fix itNothing to fix if the domain is intentionally mail-only. If a website is supposed to exist, add A/AAAA records for the domain or www and re-run the scan.

HTTP response reachable #

The header checks evaluate the response of a real page load (https://…/). If no HTTP response can be fetched – connection refused, timeout, or an invalid certificate aborting the request – there are no headers to grade. A fourth case is named separately: the server answers, but this scanner’s HTTP client discards the response as malformed. Node does that for an entire response as soon as one header field value carries a control character. RFC 9110 §5.5 calls such values invalid but leaves recipients the choice — for CTLs other than CR, LF and NUL it says recipients MAY retain them — and Chromium and Gecko take that option – so the finding then says what happened on our side instead of calling the site unreachable.

RatingOnly appears when the page load fails: skip; the module is reported as not gradeable instead of being graded F on missing data. If neither the domain nor www has any A/AAAA records at all, the more precise “Website configured (A/AAAA records)” entry appears instead.
How to fix itMake sure the site answers HTTPS requests with a valid certificate and within a reasonable time, then re-run the scan. The Website module usually shows the underlying TLS problem in detail.

Redirect target #

The scanner follows redirects before analyzing headers. If the chain ends on a different domain, all header findings describe that final host, not the scanned domain. If it ends on unencrypted http://, the entire page (content, cookies, logins) is exposed to man-in-the-middle attacks despite the HTTPS start.

Ratingfail + Cap C if the final URL is http:// (HTTPS→HTTP downgrade); info if the redirect merely ends on another registrable domain. info as well when the redirect stays inside the same registrable domain but lands on a different host (the common apex → www): every header finding below then describes that host. info also when the chain is still redirecting after the fifth hop – the headers graded then belong to an intermediate 3xx response, which is a limit of this scan. Nothing is shown only when the page stays on the SAME host over HTTPS.
How to fix itRemove any redirect to http:// and serve the final page over HTTPS. For cross-domain redirects, scan the final domain directly for meaningful results.

Strict-Transport-Security (HSTS) #

HSTS instructs the browser to access the domain exclusively over HTTPS from then on, preventing SSL-stripping/downgrade attacks on every later visit. It cannot protect the first contact with a host the browser does not yet know: until an HTTPS response carrying the header has been seen — either from the host itself, or from a parent domain that sent includeSubDomains, which covers a subdomain from its very first request (RFC 6797 §8.3 step 5) — the browser does not know the rule, and that gap is exactly what the preload list closes.

Ratingpass with max-age ≥ 1 year; warn with a shorter lifetime; missing → fail. Special case: max-age=0 → fail, because that actively DELETES an existing HSTS policy in the browser. Second special case: an HSTS header delivered on a response that ended on unencrypted http:// → fail, because browsers process the header only over a secure transport (RFC 6797 §8.1) – it has no effect there, whatever its max-age says. Weight 2.
How to fix itSet the header: Strict-Transport-Security: max-age=63072000; includeSubDomains. Add preload only if you deliberately want the domain hard-coded into browsers and have verified HTTPS on every subdomain – the directive is a submission consent, and while getting onto the list is easy, leaving it takes months of browser releases; see the HSTS preload entry.

HSTS preload #

Domains on the browsers’ HSTS preload list are addressed only over HTTPS from the outset – even on the very first visit, before any header is seen. This point checks two independent facts: (1) eligibility – does the current header meet the hstspreload.org criteria (max-age ≥ 1 year, includeSubDomains, preload)? and (2) list status – is the domain actually on the preload list, queried live from the official hstspreload.org status API? The critical case is being on the list while the header is no longer compliant (e.g. preload was removed later): shipping browsers still enforce HTTPS, but the entry becomes eligible for automatic removal – after which first-visit protection silently disappears – and the header should never have been changed without the official removal process.

Ratinggood when preloaded with a still-compliant header, when the protection comes from an ancestor entry (e.g. a preloaded TLD with includeSubDomains – the host’s own header cannot change that entry), or eligible-but-not-yet-submitted (submitting is optional, weight 0); info when not preloaded and not yet eligible; warn (weight 1) for a pending submission/removal whose header no longer matches; fail (weight 2) when the domain is on the preload list but the current header is non-compliant – and only when the listed host is also the host whose header was read: after a redirect to another host the header belongs to that host, so the finding drops to info (weight 0) and names the gap instead of accusing. If the hstspreload.org status API does not answer, the finding says the list status could not be determined rather than claiming the domain is not preloaded.
How to fix itTo get preloaded: meet all criteria and submit at hstspreload.org. If already preloaded, keep serving max-age=63072000; includeSubDomains; preload unchanged; to leave the list, use hstspreload.org/removal/ rather than just dropping the header.

Content-Security-Policy #

The CSP is the most effective protection layer against cross-site scripting (XSS): it defines which sources may load scripts, styles, etc. A pure host allowlist with unsafe-inline is often bypassable; nonce-/hash-based policies with strict-dynamic are strong. The detail additionally reports whether the strict internet.nl criteria are met (no unsafe source in ANY directive incl. style-src, no http:/bare https:/wildcard hosts, restricted default-src/frame-ancestors/base-uri/form-action) – relevant for a 100% internet.nl score, without influence on the grade here. The criteria are not paraphrased but compared: a verbatim extract of the internet.nl checker is run beside ours over the same policies, so where the two differ it is a decision with a reason rather than a drift. Two of those differences are deliberate and in your favour — a wildcard host is only accepted with a label boundary, and a frame-src falling back to a default-src * is not treated as restricted.

Ratingpass with a strong (nonce/hash) CSP or an allowlist without unsafe sources; warn with unsafe-inline/wildcards without nonce, with Report-Only only, when no script-restricting directive is delivered at all (script-src-elemscript-srcdefault-src all absent – scripts are then unrestricted), and when the CSP arrives only via a <meta> tag (frame-ancestors, sandbox and reporting are ignored there); fail only when no policy is delivered in any form. Weight 2.
How to fix itUse a nonce-/hash-based CSP with strict-dynamic; avoid unsafe-inline/unsafe-eval (inline styles → CSS files or element.style/CSSOM).

CSP hardening (object-src / base-uri / form-action) #

Three directives harden a CSP against injection beyond script sources: object-src 'none' blocks outdated plugin embeddings (<object>/<embed>). base-uri and form-action have NO fallback to default-src – without them set, an injected <base> tag can redirect relative URLs to a foreign server, and an injected <form action> can send data to foreign servers (phishing/exfiltration), even with a strict default-src.

Ratingpass if all three are restricted (object-src/default-src 'none'; base-uri 'none'/'self'; form-action 'self'/'none'); otherwise warn listing the missing directives. Only with an enforced CSP. Weight 2.
How to fix itAdd object-src 'none'; base-uri 'none'; form-action 'self' to the CSP.

CSP upgrade-insecure-requests #

Automatically upgrades insecure HTTP resource requests to HTTPS, thereby preventing mixed content. The older block-all-mixed-content is deprecated.

RatingInformational (weight 0): indicates whether the directive is active.
How to fix itAdd upgrade-insecure-requests to the CSP (replaces block-all-mixed-content).

CSP Trusted Types #

Trusted Types (require-trusted-types-for 'script') are the strongest protection layer against DOM-based XSS: dangerous DOM sinks then accept only vetted values.

RatingOnly reported when the policy mentions Trusted Types at all. good (weight 0) when require-trusted-types-for 'script' actually turns enforcement on; info when only a trusted-types directive names allowed policies – that declares policy names but enforces nothing, the DOM injection sinks still accept plain strings. No deduction if missing (advanced).
How to fix itOptionally set require-trusted-types-for 'script' (+ trusted-types).

CSP sandbox #

The sandbox directive additionally restricts the document (like the iframe sandbox attribute). Caution: allow-scripts + allow-same-origin together largely defeat the sandbox.

Ratinggood if restrictive; warn with allow-scripts + allow-same-origin. Weight 0.
How to fix itDo not allow allow-scripts and allow-same-origin together.

CSP violation reporting #

CSP violations can be reported – in the modern form via report-to <group> (references a defined reporting endpoint), in the deprecated form via report-uri <url>. If report-to references an undefined group, all reports are silently lost.

Ratinggood (green check, not graded) with a valid report-to; warn with report-to pointing to an undefined endpoint; info if only the deprecated report-uri is used – it is still evaluated on its own, so those reports do arrive. Weight 0.
How to fix itSet report-to to a group defined in Reporting-Endpoints (add report-uri as well for the transition).

X-Content-Type-Options: nosniff #

Prevents MIME sniffing – the browser then interprets responses strictly according to the declared Content-Type instead of "guessing", which blocks certain XSS tricks.

Ratingpass with nosniff; otherwise fail. Weight 1.
How to fix itSet the header: X-Content-Type-Options: nosniff.

Clickjacking protection #

Prevents the page from being embedded in a foreign frame and abused for clickjacking – via X-Frame-Options or, in the modern form, via CSP frame-ancestors.

RatingThe VALUE counts, and CSP frame-ancestors always wins: as soon as any enforced policy carries it, browsers ignore X-Frame-Options entirely (HTML Standard §7.7) – a DENY included. pass with frame-ancestors none/self/host allowlist, or, when no frame-ancestors is delivered, with X-Frame-Options: DENY/SAMEORIGIN; pass as well when several contradictory XFO values arrive and at least one of them is DENY, SAMEORIGIN or ALLOWALL, because browsers then block the page (fail-closed) – with a note to clean the configuration up. A set of purely INVALID values is not that case: HTML Standard §7.7 treats it like no header at all. warn with frame-ancestors */scheme wildcard. fail when both are missing, and when the only XFO value is something else (ALLOW-FROM, ALLOWALL, a typo): that means embedding is allowed from anywhere, the same exposure as sending no header at all. Weight 1.
How to fix itSet X-Frame-Options: DENY or CSP frame-ancestors 'none' (frame-ancestors is the modern, more powerful option).

Referrer-Policy #

Controls how much of the originating URL is sent to third parties when leaving the page. Without a header there is no leak: every current engine defaults to strict-origin-when-cross-origin (Chrome 85, Firefox 87, Safari 15), so cross-origin requests carry only the origin. What an explicit header buys is that the behaviour no longer depends on the browser. Some values are genuinely unsafe: unsafe-url always sends the full URL, origin-when-cross-origin leaks the path within the origin, and no-referrer-when-downgrade sends the full URL – path and query included – to every equally or more secure destination, third-party HTTPS sites included; it withholds the referrer only on an HTTPS→HTTP downgrade, which is the one case its name describes.

RatingThe VALUE counts (not just its presence): pass with no-referrer / same-origin (fully privacy-protecting) as well as strict-origin / strict-origin-when-cross-origin (pass with a tightening recommendation – internet.nl rates these two as recommendation-level only); warn with origin / origin-when-cross-origin / no-referrer-when-downgrade / unsafe-url, and warn for a missing header – every current engine then falls back to strict-origin-when-cross-origin (the Fetch default since the November 2020 spec revision), so no full URL leaks; what is missing is the explicit, engine-independent hardening. With multiple (fallback) values, the last one understood by the browser counts. Weight 1.
How to fix itSet Referrer-Policy: no-referrer or same-origin; strict-origin-when-cross-origin is an acceptable minimum.

Permissions-Policy #

Selectively disables powerful browser features (camera, microphone, geolocation …) and reduces the attack surface – including for embedded content. Note the reach: only Chromium evaluates this header, so it hardens for part of your visitors rather than all of them – Firefox reads it nowhere in its production code, and WebKit implements only the allow attribute on an iframe, not the policy this header declares (its source says so in as many words: "Declared policy is not implemented yet", checked 2026-08). The header itself is not a vendor extension: it is a W3C Working Draft (Permissions Policy, revision of 18 June 2026). Its absence is not an open door either – every feature has a default allowlist, and for the powerful ones that default is self. Allowlist syntax: () = off, self = own domain only, * = everywhere (risk), (self "https://…") = allowlist. Watch the quotation marks: self is a token and must stand bare – ("self") switches the feature off completely, and 'self' (CSP-style quotes) is not valid structured-field syntax at all, so the whole header fails to parse – the browser discards it, every other directive in it included. A feature name the browser does not KNOW is a different case, and the difference decides how much survives: an unknown member is still a well-formed structured-field entry, so the browser keeps the rest of the header and only writes Unrecognized feature: 'x' into the console. Measured in Chrome 152 across four states, with a control that can see the permissive outcome: without the header camera is allowed, with camera=() it is blocked, with camera=() plus an unknown member it is still blocked, and with the malformed camera =() it is allowed again. A console warning of this kind therefore means ONE member is inert; a parse error means none of them apply. Inert is not the same as harmless: if the unknown name is a TYPO (camrea=() for camera=()), the restriction you meant is simply absent while the rest of the header parses normally – measured, camrea=(), microphone=() blocks the microphone and leaves the camera at its default. The warning is worth reading: it says either that the browser lacks the feature or that the name is misspelled, and only the operator can tell the two apart. And recognition is a property of the browser BUILD, not of the specification: bluetooth=() is in the specification and is reported as unrecognised by a Chrome whose Web Bluetooth is compiled out (measured, Chrome 152 on Linux), so the warning alone does not make the header wrong – it only means that one visitor’s browser cannot act on that one member.

Ratingpass if the core features (camera/microphone/geolocation/payment/usb) are disabled; warn with wildcard * on a core feature, a missing header, empty/permissive values, outdated Feature-Policy syntax, a header that does not parse as a structured-field dictionary (RFC 9651) – then browsers discard the WHOLE header and nothing is restricted at all; the usual cause is a space next to the =, as in camera =() or camera= () – a further high-risk feature (display-capture, serial, hid, bluetooth …) opened up with *, or a quoted self – it is a token and must stand bare, ("self") switches the feature off entirely and 'self' is not valid structured-field syntax at all, so the browser discards the whole header. A high-risk feature that is merely not mentioned is usually not the biggest gap: its default allowlist is self, so a cross-origin frame only gets it if the page itself delegates it with an allow attribute on the iframe – and exactly that delegation is what an explicit camera=() would still block. Weight 1.
How to fix itSet unneeded features to (), e.g. Permissions-Policy: camera=(), microphone=(), geolocation=().

Cross-origin isolation (COOP/COEP) #

COOP isolates the browsing context from cross-origin opened windows and makes XS-Leaks harder. The value is decisive: unsafe-none provides no protection, while same-origin, same-origin-allow-popups and noopener-allow-popups do – the last severs the opener relationship for windows this page opens while leaving popups usable. FULL cross-origin isolation (for SharedArrayBuffer/precise timers) additionally requires COEP require-corp/credentialless; CORP protects your own resources from being included by foreign sites. The COEP/CORP state is reported as context in the same finding – it is optional for many sites and potentially breaking.

RatingInformational only – NOT graded. All three headers are established: COOP and COEP ship in Chrome 83, Firefox 79 and Safari 15.2, CORP in Chrome 73, Firefox 74 and Safari 12, and the MDN HTTP Observatory rewards each of them (+10 points, −5 for an invalid value). We still do not grade them, because requiring COOP same-origin can break OAuth/payment popups on ordinary sites: cross-origin isolation is a deliberate architectural decision, not a baseline. A protective COOP value (same-origin, same-origin-allow-popups, noopener-allow-popups) earns a green check (good) – but only on an HTTPS page, since browsers ignore COOP outside secure contexts (HTML Standard §7.1.3.1 returns the default policy for a non-secure context, and Gecko implements exactly that). noopener-allow-popups is the newest of the three and is not universal: Firefox's COOP parser knows only same-origin and same-origin-allow-popups (checked 2026-08 with a positive control), so there the value falls through to unsafe-none and protects nothing. unsafe-none, a missing header, an unknown value and a header delivered twice (browsers parse it as a single structured-field item, that parse fails, and unsafe-none applies) are neutral info. All values are matched case-sensitively. COEP/CORP are reported as context.
How to fix itOptional hardening only. Set Cross-Origin-Opener-Policy: same-origin if you rely on cross-origin isolation (SharedArrayBuffer, precise timers); for full isolation additionally Cross-Origin-Embedder-Policy: require-corp and appropriate CORP headers. Not required for a typical site.

Origin-Agent-Cluster #

With Origin-Agent-Cluster: ?1 the page asks the browser to key its agent cluster by origin rather than by site. The observable effect: document.domain stops relaxing the same-origin restriction, and WebAssembly modules can no longer be sent to same-site cross-origin documents. A browser may honour the request by allocating a separate process, but it is not required to – MDN states explicitly that this "should not be viewed as a security feature", and the protection against Spectre-style side-channel attacks is what cross-origin isolation (COOP+COEP) provides, not this header. Any value other than ?1?0 in particular – is the opposite request.

RatingThe value counts, not the presence: good (weight 0) for ?1; info for ?0, which explicitly opts OUT of origin-keyed agent clusters (Chrome has keyed by origin by default since version 119 – the compile-time default is off through 118, and what happened in 115 was a field trial behind a flag; measured on Chrome 152, a page sending no header at all reports originAgentCluster === true –, so ?0 asks it back to site-keying and keeps the legacy document.domain relaxation usable – a legitimate compatibility switch, but the opposite of hardening); info for any other value, which browsers ignore. Never graded: the HTML standard states this is not a security feature.
How to fix itOptionally set Origin-Agent-Cluster: ?1.

Document-Policy #

Document-Policy configures document-wide behaviour – a sibling of the Permissions-Policy, but for document properties rather than device features. It is a Chromium-incubated proposal that has stalled: the WICG specification has received no commits at all since March 2022 (checked 2026-08). Safari implements nothing of it: WebKit — which is published, unlike the TLS engine — knows neither the header nor any of the shipped points (source checked 2026-08), and WebKit’s own recorded test expectation for force-load-at-top is FAIL. Firefox implements exactly one point: it reads the header, parses it as a structured-field dictionary and honours force-load-at-top for its text-fragments implementation – and nothing else. Only a handful of points ever shipped – force-load-at-top, expect-no-linked-resources and js-profiling, which gates the JS Self-Profiling API – while the image-related policies that are often quoted (oversized-images, unoptimized-lossy-images) never left the flag stage.

RatingInformational (weight 0) if set.
How to fix itOptionally use it depending on the use case.

Mixed Content #

If an HTTPS page loads resources over http://, an attacker can tamper with them. Active mixed content (scripts/iframes/CSS) would allow takeover of the page and is therefore blocked outright by every current browser; passive content (images, media) is upgraded to HTTPS where possible and blocked when that fails. The finding is thus less about what still loads than about a configuration that relies on the browser cleaning up after it.

Ratingactive mixed content → fail; passive → warn; none → pass; info ("not determined") when the page exceeded our 256 KB read limit and was truncated – a resource that was found still counts, because presence needs no completeness while an all-clear does. Only real elements are counted: markup inside a comment, or inside the body of a <script> or <style> block, is text for the browser and requests nothing, so it is skipped here too. Comment endings follow the HTML standard, --!> included; only an UNTERMINATED <!-- is deliberately read past, because treating the whole rest of the page as a comment would hide every later finding on it. Markup inside a <template> IS counted – it fetches nothing until a script clones it, but that is what templates are for, and after the clone the request is real. Attribute values are read the way a browser reads them, with HTML character references resolved, so a src written as &#104;ttp://… is not mistaken for something harmless. upgrade-insecure-requests counts as a mitigation and is read both from the CSP header and from a <meta http-equiv> tag, which really does enforce this directive. Weight 1–2.
How to fix itInclude all resources over HTTPS or set CSP upgrade-insecure-requests.

Subresource Integrity (SRI) #

Cross-origin included scripts/styles (e.g. from CDNs) without an integrity hash are vulnerable to supply-chain tampering: if the CDN is compromised, attacker code runs on the page.

Ratingpass if every cross-origin script carries an integrity attribute; warn as soon as one does not; info ("not determined") when the page exceeded our 256 KB read limit – a script found without integrity still counts, an all-clear needs the full page. Only the presence of the attribute is checked, not whether its hash is correct or its algorithm still acceptable; commented-out tags are ignored (with the same comment rules as above), as is markup that exists only inside a <script> or <style> body – the parser reads those as raw text, not as markup. That holds for the HTML namespace: inside <svg> or <math> the same tags are not raw text, and markup within them is parsed normally — this scanner skips it there too, which can only mean it reports less, never more. Weight 1.
How to fix itAdd integrity="sha384-…" crossorigin="anonymous" to cross-origin <script>/<link>.

Integrity-Policy #

Integrity-Policy enforces Subresource Integrity for the listed destinations: resources without integrity metadata are blocked – supply-chain protection that goes beyond the individual integrity attribute. Integrity-Policy-Report-Only reports violations without blocking anything: a dry run, not protection.

Ratinggood for the enforcing header, info for Report-Only – weight 0 either way, and no deduction if missing. The header is young (Chrome 138, Firefox 145, Safari 26), and enforcing it requires every included resource to carry integrity metadata.
How to fix itOptionally set Integrity-Policy: blocked-destinations=(script).

Cookie security attributes #

Cookies without Secure can be intercepted over HTTP; without HttpOnly they can be read via JavaScript (XSS); SameSite protects against CSRF.

Ratingpass if all set cookies carry Secure + HttpOnly; warn if one of the two is missing; fail if a cookie uses SameSite=None without Secure – either way the cookie is broken: Chrome and Firefox reject it outright (RFC 6265bis §5.7 step 19), while Safari 26.6 still accepts it and then sends it unprotected over HTTP (web-platform-tests, aligned stable run 2026-08). A cookie with no EFFECTIVE SameSite value is named in the detail (Chromium then treats it as Lax, Firefox does not apply that default – checked 2026-08, and it is a preference there rather than a fixed rule) but does not change the verdict – effective means the attribute is present AND its value is one of Strict/Lax/None, because anything else falls back to the same default as no attribute at all. Attribute names and values are read the way a browser reads them (RFC 6265bis §5.6): only spaces and tabs are trimmed, so Secure preceded by a non-breaking space is an unknown attribute and does not count. Weight 1.
How to fix itServe cookies with Secure; HttpOnly; SameSite=Lax (or Strict).

Cookie name prefixes (__Host-/__Secure-) #

The name prefixes __Host- and __Secure- (RFC 6265bis) let the browser enforce cookie integrity: a __Secure- cookie must be set with Secure, a __Host- cookie additionally with Path=/ and without a Domain attribute. Violating cookies are discarded by browsers without any signal to the server – the application then never receives them, which surfaces as mysterious login loops. Firefox does log it in the web console (“has been rejected for invalid prefix”), so look there before hunting on the server side.

Ratingwarn when a prefixed cookie violates its preconditions. Weight 1. Correctly used prefixes are simply part of a pass in the cookie check.
How to fix itSet Secure (and for __Host-: Path=/, no Domain=) – or use the prefix deliberately: __Host-session=…; Secure; HttpOnly; SameSite=Lax; Path=/ is the most robust session-cookie form that RFC 6265bis standardises. Chrome and Firefox additionally enforce an unstandardised __Host-Http- prefix that pins HttpOnly as well; it appears in neither draft-22 nor the editor’s copy, and Safari 26.6 implements only part of it (5 of 7 web-platform-tests, against 7 of 7 in both others).

Cache-Control on authenticated responses #

Authenticated responses (with a session/auth cookie) should not be cached in proxies/browsers – otherwise subsequent users could see other people’s logged-in content.

Ratinginfo (weight 0): checked ONLY when the FINAL response – the one whose Cache-Control header is read – itself sets a session/auth cookie and is not no-store/private. A cookie set earlier in a redirect chain does not count: cookie and header have to describe the same response, otherwise a login redirect produces a finding about a page that never set the cookie.
How to fix itServe authenticated responses with Cache-Control: no-store (or private).

CORS (Access-Control-Allow-Origin) #

A misconfigured CORS policy can allow foreign sites to read authenticated responses. The dangerous combination is a reflected origin – or the literal null – together with Access-Control-Allow-Credentials: true. A plain * is not part of that: browsers never honour the wildcard for a credentialed request, so nothing can be read out that way.

Ratingfail (weight 2) only with null + credentials – a document in an iframe whose sandbox does not contain allow-same-origin has an opaque origin and therefore sends exactly the origin null, so authenticated responses really are handed over. warn (weight 1) with * + credentials (a contradictory configuration: browsers block the combination, so nothing leaks, but one of the two settings is ineffective), with a fixed origin + credentials (fine as such – critical only if the server reflects arbitrary Origin values, which a passive header check cannot decide), and with invalid ACAO syntax such as several origins or a wildcard subdomain (matches no origin at all). info with * without credentials – the intended form for public resources. Only checked if ACAO is set.
How to fix itNever combine a reflected origin/* with Allow-Credentials: true; use a strict origin allowlist instead.
Sources: MDN: CORS ↗

X-Permitted-Cross-Domain-Policies #

Controls whether Adobe clients (Flash/Acrobat) may access across domains via crossdomain.xml. Best practice is none. Real-world relevance is low (Flash is dead), but the OWASP Secure Headers Project still lists it among the headers to set, with none as the recommended value.

Ratingpass with none; warn with all; info for any other value (e.g. master-only). Only evaluated when the header is set. Weight 0.
How to fix itOptionally set X-Permitted-Cross-Domain-Policies: none.

X-XSS-Protection (deprecated) #

The browsers’ former XSS auditor has been removed and could itself cause security problems (XS-Leaks). The header should no longer be used actively.

Ratinginfo (weight 0) if set to 1/1; mode=block – recommendation to remove it or set it to 0.
How to fix itSet the header to 0 (or remove it) and use a CSP instead.

HTTP Public Key Pinning (HPKP) #

HPKP was meant to prevent certificate mis-issuance, but the risk of permanently locking yourself out was so high that every browser removed it (Chrome in 72, Firefox in 72–78; Safari and Edge never shipped it). Because nothing pins any more, the header today neither protects nor endangers – it is dead configuration. Mis-issuance is addressed one layer at a time instead: CAA constrains which CA may issue at all (checked by the CA, RFC 8659 §4), and Certificate Transparency makes an issued certificate publicly discoverable. Neither is a browser-enforced pin, so neither is a drop-in replacement. Note that RFC 7469 itself is still a Proposed Standard: what is deprecated is the browser support, not the specification.

Ratinginfo (weight 0) if Public-Key-Pins(-Report-Only) is set. Since no browser evaluates HPKP any more, the header cannot cause harm either – there is nothing left to deduct for. Removing it as dead configuration is still worthwhile.
How to fix itRemove the HPKP header (use Certificate Transparency + CAA instead).

Expect-CT (deprecated) #

The Expect-CT header has been removed since Chrome 107 – Certificate Transparency is now enforced automatically. The header is superfluous.

RatingInformational (weight 0).
How to fix itRemove the Expect-CT header.

Violation/error reporting #

Reporting delivers reports about CSP/COOP/network violations. There are two generations: Reporting-Endpoints (modern, preferred) and the deprecated Report-To (removed from the Reporting specification and listed as deprecated/non-standard on MDN, yet still evaluated – Chrome 70+, Firefox 149+, not supported by Safari). Important: NEL (Network Error Logging) works ONLY with Report-To, not with Reporting-Endpoints.

Ratinggood (weight 0) when a reporting mechanism is configured; info when only the legacy Report-To is used – a migration note, not a defect; warn if NEL references a Report-To group that is not defined (reports are silently lost), and warn if the Reporting-Endpoints header is not a valid structured field (RFC 9651): browsers then discard it entirely and no endpoint group exists, the usual cause being a space around the =. If the Report-To header cannot be parsed, the group comparison is skipped rather than guessed. Two more shapes were measured directly against Chrome 152 rather than derived from the grammar: a member whose value is not a quoted string (good="https://…", bad=sometoken) makes Chromium discard the whole field, so it counts as unparseable here even though the specification text says to skip just that member — and that is a deliberate choice against the other two engines, not a gap. All three read this header (Chromium since 96, Safari since 16.4, Firefox since 149 on the rapid-release channel — ESR 140 still ships the Reporting API disabled), and on this one shape Firefox and WebKit both follow §3.3 and skip only the offending member, so their remaining endpoints keep working. This report follows the strictest of the three, because the error costs are lopsided: reporting a working header as broken costs a hint, while trusting a group Chrome never registered would be a false all-clear. Firefox and Safari were read from their source here, not run; and a member whose URL is not a potentially trustworthy one — in practice: not https:// — is dropped on its own while the rest of the header keeps working (W3C Reporting API §3.2 – browsers ignore a non-secure endpoint), which is warn and names the group, because reports aimed at it are silently lost.
How to fix itSet Reporting-Endpoints; for NEL, additionally define a matching Report-To group.

Technology disclosure (Server / X-Powered-By) #

Headers like Server: nginx/1.26.3 or X-Powered-By: PHP/8.2 reveal the software stack. A version number lets an attacker match known CVEs directly to the exact build – that is what matters and what gets downgraded. A bare product name (Server: nginx, AmazonS3, cloudflare, Apache) only says WHAT runs, not which version, so it cannot be mapped to a specific CVE and is treated as purely informational (no downgrade). A "version" means a dotted decimal (e.g. /1.26.3, 4.0.30319); a product name that merely contains a digit (AmazonS3, CDN node IDs) is not a version. Hiding banners entirely is hygiene (minimal disclosure), not a substitute for patching.

Ratinggood when no technology headers are sent; info with product names only, even when the name contains a digit like AmazonS3 (weight 0, no downgrade); warn only when an actual version number (dotted decimal) is disclosed (weight 1). Checked: Server, X-Powered-By, X-AspNet-Version, X-AspNetMvc-Version, X-Generator.
How to fix itnginx: server_tokens off (hides the version) or remove entirely with the headers-more module (more_clear_headers Server); Express: app.disable("x-powered-by"); PHP: expose_php = Off.

security.txt (RFC 9116) #

A /.well-known/security.txt provides contact channels for security reports and facilitates responsible disclosure of vulnerabilities. The file is validated against the MUST/SHOULD requirements of RFC 9116 the way internet.nl’s sectxt validator does: mandatory Contact (as a URI – a bare email address without mailto: is invalid, §2.5.3), exactly one Expires in RFC 3339 format that is not in the past (§2.5.5), text/plain with UTF-8 (§3), https:// for all web URIs, valid field syntax, at most one Preferred-Languages with valid tags, the mandatory /.well-known/ location (§3) and – if Canonical is present – that the retrieved URI is listed there (§2.5.2, otherwise the file should not be trusted). SHOULD requirements (Expires less than a year ahead, OpenPGP signature, Encryption key for email contacts) are reported as hints. Beyond the structural checks, the OpenPGP layer is verified cryptographically (RFC 9580): the key block published under Encryption is fetched and assessed (usable public key, RSA key size, ROCA/Fermat factorability up to 4096 bits – above that the report states that the screen did not run, rather than reporting a clean key –, no leaked private-key block), and the cleartext signature is verified against the keys contained in that published block. Whether a key inside the block really is a subkey of the primary one is verified: the 0x18 subkey binding signature has to hold against the primary key, and for a SIGNING subkey the embedded 0x19 back signature has to hold against the subkey as well (RFC 9580 §5.2.4, §5.2.3.34) – without the latter, anyone could bind a foreign signing key into their own certificate. Only then does the report call the signer a subkey of the published key; a key packet that carries no valid binding is named as what it is, a key that sits in the served block without belonging to that certificate. A user ID is held to the same standard: the report only says that a block certifies an address when the 0x10-0x13 self-certification of that user ID verifies against the primary key and no newer 0x30 revocation supersedes it (RFC 9580 §5.2.4, §5.2.1) - on a block fetched from a third-party directory the address is otherwise just text in a packet nobody signed. Whether a signing subkey is still in force is read from the certificate as well: a 0x28 subkey revocation, and the two lifetimes of §5.2.3.13 and §5.2.3.18. A revoked or expired subkey still verifies mathematically, so the report names that instead of printing a bare “valid”. A signature that does not match the served content, depends on a forbidden hash (MD5/SHA-1/RIPEMD-160) or hides behind spoofable armor headers is flagged; a signature made by a key other than the published one is pointed out. Finally, if the file names a mailto: contact, the check looks up that address in the Web Key Directory of its own domain: a key that is only linked from the file but not discoverable via WKD cannot be found automatically by a reporter’s mail client.

Ratinggood if present and the validation reports no errors (open recommendations are listed as hints); warn as soon as at least one error-class issue is found – the same severity split internet.nl uses; info if the mandatory path answered and no usable file was found ("missing"), and info as well – but worded as "not determined" – when our own request to /.well-known/security.txt failed, or when the module’s time budget was used up before it could be sent at all, because neither a timeout nor a budget on our side is a statement about the domain. Signature and key problems join the same classes: errors for an invalid signature, a forbidden hash, a factorable or leaked key – hints for an unreachable Encryption key or a signature by a different key. Weight 0.
How to fix itProvide a file at /.well-known/security.txt with Contact: (mailto:/tel:/https: URI) and Expires: (RFC 3339, less than a year ahead), served as text/plain; charset=utf-8; if Canonical is used, list the exact retrieval URI. Sign the file as an OpenPGP cleartext signature with the key published under Encryption (SHA-256 or stronger) and re-sign after every content change. Cross-check e.g. with the internet.nl website test, which runs the same sectxt validation.

Reachable over IPv4 and IPv6 #

The page is fetched a second time, once per address family, with the connection pinned to one address each time. A single ordinary request lets the operating system choose the family, so a host that is broken on one side still answers – the check would hide exactly the defect it exists to find. Once both records are published, which family a visitor uses is decided by their network and by their client's own selection algorithm (RFC 8305), not by the operator, so a family that fails is a family some visitors are stuck with — RFC 8305 §7.2 names the case: on IPv6-only networks with NAT64/DNS64 a present-but-broken AAAA record is passed through rather than replaced. Probed is the URL the main request ended on, not the address that was typed: most sites redirect the apex to www, and a followed redirect drops the pin – both probes would then converge on the same endpoint and the comparison would stop being about address families at all.

RatingThe row appears only when there is something to report. warn (weight 1) when the host publishes only an AAAA record – clients without IPv6 cannot reach it at all; warn (weight 1) when both families are published but one of them does not deliver the page, whether it stayed silent or answered with a status outside 2xx – the row names the family, the address and what came back. skip – shown as “not testable” or “not determined” – whenever the comparison could not be made at all: this scanner has no route to that address family, its HTTP client discarded the response as malformed, the address lookup for the host gave no conclusive answer (a SERVFAIL or a resolver failure is not the same as “no records”), or the module’s time budget was used up before the comparison could start. All four are limits of ours and none of them lowers the grade – the row keeps the weight the comparison would have carried and is reported separately as an unmeasured part of the module, so that measuring less can never look like a better result. No row at all – and no deduction – when both families deliver the page, when only an A record is published (still the norm, not a defect) or when the host conclusively publishes no address at all.
How to fix itServe the page successfully over both address families, or withdraw the DNS record of the family that does not work – a published record is a promise a client will try to keep.

Same security headers on IPv4 and IPv6 #

When both address families deliver the page, their responses are compared – not the page content, but the security headers: Strict-Transport-Security, Content-Security-Policy, Content-Security-Policy-Report-Only, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, Cross-Origin-Opener-Policy, Cross-Origin-Embedder-Policy and Cross-Origin-Resource-Policy. Headers rather than bodies on purpose: a live HTML page legitimately differs between two requests (session ids, tokens, timestamps, A/B buckets), so comparing bodies would be a false-positive machine, while these headers are configuration. A site served from two edges can carry a different policy per family, and nothing else in the scan would surface it – the main website module probes both families at the socket level but never looks at HTTP headers.

Ratingwarn (weight 1) when at least one of the compared headers differs between the families; the value line names which ones. Whichever set a visitor gets depends on their routing, so the weaker set is the one that counts. Three cases are deliberately not compared, because the comparison would then not be about address families at all: a CSP nonce is regenerated per request by design, so it is normalised away before comparing – without that, every site using nonces would be reported; the query string of a report-uri or report-to endpoint is collapsed for the same reason, because it routinely carries a per-request id – measured over the 60 hosts of this project’s page corpus, each fetched twice over one single address so that a family difference cannot exist, 53 were comparable and 3 were flagged, every one of them for exactly that; and the whole comparison is dropped when a redirect was followed, because both probes then converge on the same endpoint. Only the query collapses – scheme, host and path of the report endpoint keep being compared, so an endpoint that genuinely differs per family is still found. No row when the headers match or when fewer than two families answered.
How to fix itAlign the header configuration of both endpoints. First check whether the difference is about the address family at all: a header set that changes with a bot challenge differs between any two fetches of the same URL, and this comparison cannot tell that apart. If the difference persists, one edge or one origin was as a rule not updated with the others; deploy the header configuration from one source rather than per host.

← All checks & scoring methodology