📎 Unreleased docs. This is the next version (from main), features here may change or not be in a release yet.See the latest release →

Scanners

draugr-headers

On this page
  • Control: headers
  • Tool: native, no external tool. A Go HTTP client fetches each host and evaluates its response headers against the OWASP Secure Headers guidance.
  • Status: ✅ implemented
  • Target: a running endpoint (HostTarget), a component's hosts:
  • Effects: network (sends an HTTP GET to the endpoint and follows its redirects)
  • License / terms: native Draugr code (Apache-2.0). OWASP Secure Headers Project is a reference only, no code or data is bundled.

What it does

Performs a GET on each host (following redirects, so it evaluates what a client actually receives) and emits one SARIF result per missing or misconfigured security header. A browser host is asked for HTML the way a browser asks, and the page it returns is read too, up to 2 MiB, so the policy can be checked against the page. The checklist is tuned by the host's type so browser-only headers aren't flagged on APIs:

GroupApplies toChecks
UniversalallStrict-Transport-Security (HTTPS), X-Content-Type-Options: nosniff, Server / X-Powered-By disclosure
Browsertype: browser (default)Content-Security-Policy, X-Frame-Options (or CSP frame-ancestors), Referrer-Policy, Permissions-Policy
APItype: apiwildcard CORS (Access-Control-Allow-Origin: *, escalated with Allow-Credentials: true), missing Cache-Control

Severities: missing hardening headers → warning; softer recommendations and information-disclosure → note; wildcard CORS with credentials → error. See the HTTP security headers glossary entry.

Grading the Content-Security-Policy

A CSP can be present and stop almost nothing, default-src *; script-src 'unsafe-inline' 'unsafe-eval' satisfies a presence check while permitting exactly what a CSP exists to prevent. So when the header is there, its content is judged too.

RuleLevelWhat it means
csp-unsafe-inlineerroran injected <script> or event handler runs
csp-unsafe-evalerroreval() and string-to-code are allowed
csp-script-src-broaderror*, https:, data: or blob:. The payload can be hosted anywhere
csp-script-src-missingerrorno script-src and no default-src: script is ungoverned
csp-object-src-broadwarning<object>/<embed> unrestricted. A route script-src does not cover
csp-base-uri-missingwarningan injected <base> can repoint every relative script URL
csp-object-src-not-nonenoteobjects restricted but not disabled
csp-default-src-missingnoteresource types the policy does not name are unrestricted
csp-unsafe-inline-legacy-fallbacknote'unsafe-inline' present but inert
csp-no-reportingnoteno report-uri/report-to, so violations are invisible

Two CSP3 rules decide whether a weakness is real, and a checker that ignores them produces findings against the policies people were right to write:

  • a nonce or hash in script-src makes 'unsafe-inline' inert, browsers ignore it, and it is there for ones too old to understand the rest;
  • 'strict-dynamic' makes host and scheme sources inert, so a policy carrying https: alongside it is being compatible rather than permissive.

Both are reported as notes saying the value is doing nothing, rather than as flaws.

base-uri and frame-ancestors do not inherit from default-src. That is the subtlety that most often leaves a policy weaker than its author believes, so a missing base-uri is reported even when default-src is tight.

Checking the policy against the page

A policy is written for a page, and a strict one can refuse part of the page it protects. An inline event handler under a script-src with no 'unsafe-inline', or a script from a CDN the list does not name, is refused by the browser, and the site's owner sees no error. The page check reads the HTML a browser host returns and applies the policy to what it finds.

RuleLevelWhat the policy refuses
csp-blocks-inline-scriptnoteinline <script> blocks with no nonce or hash the policy lists
csp-blocks-inline-handlernoteon*= attributes and javascript: links
csp-blocks-inline-stylenote<style> blocks and style= attributes
csp-blocks-script-originnote<script src>, and preloads of script
csp-blocks-stylesheet-originnote<link rel=stylesheet>, and preloads of style
csp-blocks-image-originnote<img src>, srcset candidates, and preloads of images
csp-blocks-font-originnotepreloaded fonts

Each origin rule is one finding per kind of resource, naming every origin refused and the directive to add it to. That directive is the one governing the resource, so a policy with only default-src is told to edit default-src.

These are notes because a policy refusing its own page is a fault in the page or in the policy, and neither exposes anybody. They never fail a security gate.

A policy sent as Content-Security-Policy-Report-Only is checked when no enforced policy is present. Nothing is refused yet, so each message says what enforcing the policy would refuse.

Where csp-unsafe-inline is reported, the page supplies its evidence. The message lists the inline scripts and handlers that depend on 'unsafe-inline', or states that the page has none, in which case removing 'unsafe-inline' changes nothing the page runs.

Matching follows CSP3, including the rules a good policy relies on: a nonce or hash makes 'unsafe-inline' inert, 'strict-dynamic' admits a markup script only by nonce or listed integrity hash, an attribute matches a hash only with 'unsafe-hashes', 'self' and http: allow the upgrade to HTTPS, and * matches network schemes and not data: or blob:.

The check reads the markup the server sends. Content a script adds after the page loads, such as a tag manager's payload or a request connect-src governs, is not in that markup and is not checked.

The heuristics follow the same public guidance as Google's CSP Evaluator, implemented natively: it is a TypeScript library rather than an exec-able binary, and Draugr executes tools rather than linking them.

Notes

  • Being native, it needs no external tool, doctor requires nothing for the headers control (only network reachability to the hosts).
  • The control talks to a scanner by name, so a tool-backed alternative (e.g. OWASP ZAP passive rules, Mozilla HTTP Observatory) could serve the same control later without changing callers.
  • The ruleset is OWASP's and is not configurable per organization. A header you have decided not to set is accepted the way any other finding is, with config.exclude and a reason.

Data

Nothing. The checks are Draugr's own and travel in the binary, so a run behind an egress allowlist needs nothing permitted for this scanner beyond reaching the host it is testing.