DEV Community

Juanjo
Juanjo

Posted on

Grading a Website's Security Headers Automatically (HSTS, CSP, X-Frame-Options) in One API Call

If you've ever run a site through Mozilla Observatory or securityheaders.com and gotten a mediocre grade with no idea what to actually change, this post walks through what each header does, why the common advice ("just add CSP") is more nuanced than it sounds, and how to check this programmatically instead of one URL at a time in a browser tool.

Why security headers matter more than they look like they should

Security headers don't stop a determined attacker with a zero-day. What they do is close off entire classes of cheap, automatable attacks: clickjacking, MIME-sniffing exploits, protocol-downgrade attacks, and a large chunk of XSS. A site with good headers isn't unhackable — it's just not hanging fruit for the scanners and bots that make up the overwhelming majority of real-world probing traffic.

The headers that actually matter, and what "correct" looks like

Strict-Transport-Security (HSTS) — tells the browser to never downgrade to plain HTTP for this domain, even if a link or bookmark points there. The common mistake: setting a short max-age (or none at all), which means the protection lapses the moment a user hasn't visited in a while. max-age=31536000; includeSubDomains is the baseline serious sites use.

Content-Security-Policy (CSP) — the header with the worst reputation, because a naive CSP breaks half the page (inline scripts, third-party widgets, analytics). The honest advice: don't reach for unsafe-inline and unsafe-eval as your first move just to make errors go away — that defeats most of what CSP protects against. Start with default-src 'self' and add specific sources as things break, rather than starting permissive and never tightening it.

X-Frame-Options / frame-ancestors — prevents clickjacking by controlling whether your page can be embedded in an <iframe> on someone else's site. SAMEORIGIN is right for almost everyone; DENY if you never embed your own pages either.

X-Content-Type-Options: nosniff — stops the browser from guessing a file's type based on content instead of the declared Content-Type. Without it, a file uploaded as an "image" that's actually JavaScript can get executed in some edge cases. One line, no downside, frequently missing anyway.

Referrer-Policy — controls how much of your URL gets leaked to third parties (analytics scripts, external links) via the Referer header. strict-origin-when-cross-origin is a reasonable default: full URL on same-origin requests, just the origin cross-origin, nothing over plain HTTP.

What a grading system should actually check

A binary "present/absent" check per header is a start, but it misses the cases that matter most: an HSTS header with max-age=0 (present, but functionally disabled), a CSP that's technically set but allows unsafe-inline for scripts (present, but not doing its job), or a Referrer-Policy set to unsafe-url (present, actively leaking more than having no policy at all in some browsers' default behavior). Grading each header's value, not just its presence, is what separates a useful audit from a checkbox exercise.

Doing this programmatically instead of one tab at a time

If you're auditing more than a handful of pages — a competitor teardown, a pre-launch checklist across a multi-page site, or a recurring monitor — checking headers by hand in browser dev tools doesn't scale. A single API call can fetch the page, read the response headers, grade each one, and return a percentage score plus per-header grades:

{
  "url": "https://example.com",
  "security_score_percentage": 61.7,
  "security_headers": {
    "strict_transport_security": "max-age=300;includeSubdomains",
    "content_security_policy": "default-src 'self'; script-src 'self' 'unsafe-inline'...",
    "x_frame_options": "SAMEORIGIN",
    "x_content_type_options": "nosniff",
    "referrer_policy": "no-referrer-when-downgrade",
    "permissions_policy": "autoplay=(), camera=(), ..."
  },
  "security_header_grades": {
    "strict_transport_security": "weak",
    "content_security_policy": "weak",
    "x_frame_options": "strong"
  }
}
Enter fullscreen mode Exit fullscreen mode

Note the max-age=300 on HSTS in that example — technically present, graded "weak" because 5 minutes is far too short to matter in practice. That's the kind of nuance a presence-only check would miss entirely.

A real comparison: 61.7% vs. 93.3%

To see the scoring actually differentiate between sites, here's the same audit run against two real, live pages.

techcrunch.com — 61.7%: strong on X-Frame-Options and X-Content-Type-Options, reasonable on Referrer-Policy and Permissions-Policy, but weak on both Strict-Transport-Security (max-age=300 — five minutes) and Content-Security-Policy (contains unsafe-inline). Two present-but-weak headers are enough to drag an otherwise decent setup down by nearly 40 points.

This API's own demo site — 93.3%: strong across Strict-Transport-Security (max-age=63072000; includeSubDomains), a nonce-based Content-Security-Policy (no unsafe-inline, no wildcard sources), X-Frame-Options: DENY, X-Content-Type-Options: nosniff, and Referrer-Policy: strict-origin-when-cross-origin — only Permissions-Policy lands on "reasonable" instead of "strong", because the grading logic currently treats any non-empty policy as reasonable and doesn't have a "strong" tier for it. That's effectively the ceiling under the current heuristic, not a header that's actually missing anything meaningful.

The gap between those two scores is the entire point of grading values, not just presence — both sites have most of these headers, but one of them actually enforces what the header is supposed to do.

This is what the Web Metadata & Contact Extractor API's /api/v1/security endpoint returns — one call, real per-header grading instead of a pass/fail, and it composes with the same anti-SSRF-protected fetch layer as the rest of the API, so it's safe to point at user-submitted URLs. It's MIT-licensed on GitHub if you'd rather read the grading logic or self-host it, and there's a live demo if you just want to check a URL right now.

Top comments (1)

Collapse
 
alexshev profile image
Alex Shev

Security-header grading is most useful when it explains tradeoffs, not only pass/fail. CSP especially needs context because a stricter policy can be correct technically and still break the product if rollout is blind.