Security Headers Scanner
Browsers only enforce protections the server asks for. The scanner fetches your pages, reads the response headers and reports each protection that is absent plus the headers that reveal software versions.
One response, eight checks
The scanner sends a plain GET and looks only at the response headers. Five checks are about a header being absent, one about a header that must hold a specific value, one pair about a header that gives too much away. The same checks run on every URL it tests.
1The scanner requests a URL and lower-cases the header names. Everything below is decided from this one response. Three headers were never sent, and two are sent with details a visitor should not need.
2Presence checks. HSTS, Content-Security-Policy, Permissions-Policy and Referrer-Policy are flagged only when the header is absent. A missing Strict-Transport-Security is HIGH at 0.95, the others MEDIUM or LOW.
3Two checks with logic. Clickjacking is reported only when both X-Frame-Options and a CSP frame-ancestors directive are missing. X-Content-Type-Options must equal nosniff; a present header with another value is flagged too.
4Disclosure checks. Server is flagged only when it contains a version such as /1.19; a bare nginx passes. X-Powered-By is flagged whenever it is present. Both are LOW, 0.90.
One finding per server, not per URL
Security headers are usually set once for the whole server. If the scanner tests several URLs it would report the same gap on each, so a deduplication pass collapses the repeats.
1Three URLs were tested and raised ten findings between them. HSTS, CSP and Permissions-Policy are missing on every URL; the Server version leaks on one.
2Findings are grouped by title. A group with as many members as there are URLs that produced findings is reported once, with a note that the header is missing server-wide. Ten findings drop to four.
3The Server version is present on one URL out of three, so it stays as its own finding with that URL. A header that differs by route is not merged away.
What the fix block clears
Because the headers are server-wide, one configuration block removes most findings at once. A few things it does not touch.
1A scan of an unconfigured server lists a finding for each of the six missing headers plus the two disclosure headers.
2Adding Strict-Transport-Security and Content-Security-Policy clears the High finding and one Medium.
3The other four headers clear the clickjacking, nosniff, Referrer-Policy and Permissions-Policy findings.
4The block shown in the markdown sets only the six security headers. Server and X-Powered-By are produced by the web server or framework, so they need their own change.
Why it matters
Missing headers make other attacks easier. No CSP lets an XSS load external scripts and exfiltrate data. No HSTS lets a man in the middle strip HTTPS. No X-Frame-Options lets an attacker embed your site in an iframe and trick users into clicking hidden buttons. An exposed Server version tells an attacker which CVEs to try.
The Magecart XSS attack was worsened by the absence of a Content Security Policy. A CSP with script-src 'self' would have blocked the injected third-party script from executing.
Vulnerable vs fixed code
// BAD: nothing sets security headers, and the framework advertises itself
const app = express() // replies with X-Powered-By: Express
app.get("/", (req, res) => res.send(page))// next.config.js
const securityHeaders = [
{ key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains" },
{ key: "Content-Security-Policy", value: "default-src 'self'; script-src 'self'" },
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "X-Frame-Options", value: "DENY" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
{ key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" },
]
module.exports = {
async headers() {
return [{ source: "/(.*)", headers: securityHeaders }]
}
}Limits and what it needs
| Check | Reports when | Severity · confidence |
|---|---|---|
| Strict-Transport-Security | header absent | High · 0.95 |
| Content-Security-Policy | header absent | Medium · 0.85 |
| Clickjacking protection | X-Frame-Options absent and no frame-ancestors in the CSP | Medium · 0.90 |
| X-Content-Type-Options | absent, or present with a value other than nosniff | Medium · 0.95 |
| Permissions-Policy | header absent | Low · 0.80 |
| Referrer-Policy | header absent | Low · 0.80 |
| Server | value contains a version like /1.19 | Low · 0.90 |
| X-Powered-By | header present | Low · 0.90 |
Only presence and, for nosniff, one exact value are checked. A header that is present but weak, such as a CSP containing unsafe-inline or an HSTS with a tiny max-age, is not reported. Every distinct URL is requested, shortest URL first, with a plain GET (credentials optional: the scan's auth headers are sent if there are any). The documented dedup only merges findings that appear on every URL that produced one.