← All articles

Your CSP is strong and broken, and a grade will not tell you

August 7, 2026 · Wilson Santos · updated September 23, 2026
On this page

Point any header scanner at a site and it will grade the Content-Security-Policy. Google's CSP Evaluator, Mozilla Observatory, the header templates in every dynamic scanner: they all measure roughly the same thing, which is how strong the policy is. Does it use unsafe-inline? Are the sources wildcarded? Is object-src locked down?

Useful. And it answers a question nobody is stuck on.

The thing teams are stuck on is different: does this policy break my site?

What every tool answers

How strong is this policy? Read the header, check it for unsafe-inline, wildcards and a missing object-src, return a grade. Everything needed is in the one line of text.

What a grade cannot answer

Does it break the site? Nothing in the header says. A directive is only wrong in the presence of the thing it blocks, so answering means fetching the page and checking every resource it pulls.

Why that gap exists

Strength is a property of the policy text. You can evaluate it by reading the header, which is why every tool does.

Whether it works is a property of the policy and the page together. A directive is only wrong in the presence of the thing it blocks, an inline handler, a script from a CDN you forgot, a font, an image, a WebSocket. Answering it means fetching the page, enumerating what it loads, and checking each against the policy.

That is more work, so a grader reads one half and stays silent on the other.

Neither half answers it alone The policy header lists one allowed source. The page loads an inline handler, a script from a CDN, a font from a third party and a WebSocket. Comparing the two produces the list of things that will not load, which is the answer a team needs and which neither the header nor the page gives on its own. THE POLICY script-src 'self'; font-src 'self' graded strong, on the text alone THE PAGE 4 scripts · 2 fonts · 1 socket invisible to anything reading a header WHAT WILL NOT LOAD onclick= handler in checkout.html cdn.example.com/analytics.js fonts.gstatic.com wss://live.example.com Four things, and the header names none of them.
Strength is a property of the text. Whether it works is a property of the text and the page together, which is why one is graded everywhere and the other nowhere.

What that does to real teams

The failure mode is predictable and extremely common.

Somebody writes a strong policy. It scores well. It also blocks three things the application legitimately does, so the site breaks in ways nobody notices immediately, an analytics call, a payment widget, a font on one page.

They roll it back and ship Content-Security-Policy-Report-Only instead, meaning to fix the violations and switch it on. That was two years ago. The header is still report-only, the reports go to an endpoint nobody reads, and the scanner grades the site as having a CSP.

The alternative outcome is a policy weakened until nothing breaks, unsafe-inline, unsafe-eval, a wildcard or two, which scores badly and at least ships. Of the two, the second is often the more honest position, and it grades worse.

Three sites, and the grade tells you nothing An enforced strict policy grades A and blocks an injected script. The identical policy sent as Content-Security-Policy-Report-Only grades A as well and blocks nothing. A policy weakened with unsafe-inline, enforced, grades C and still blocks a script loaded from another origin. ENFORCED Content-Security-Policy GRADE A BLOCKS AN INJECTED SCRIPT yes REPORT-ONLY, FOR TWO YEARS …-Policy-Report-Only GRADE A BLOCKS AN INJECTED SCRIPT nothing, ever WEAKENED UNTIL IT SHIPPED …'self' 'unsafe-inline' GRADE C BLOCKS AN INJECTED SCRIPT from another origin, yes Two of these score the same. The one that scores worst is the only one of the last two doing anything.
The grade is computed from the policy text, and the middle site's text is excellent. Nothing in the header says a browser was asked to report violations rather than stop them.

A grader that cannot tell those three situations apart is grading the wrong thing.

What would help

A browser already computes this answer. Content-Security-Policy-Report-Only asks it to check every resource on the page against the policy and report what it would have blocked, which is the question, and it is the standard advice for exactly that reason.

It arrives somewhere nobody is standing. A violation report needs real traffic to produce it, a collector to receive it and a person to read it, and a header that has been report-only for two years is what becomes of the third.

Draugr runs the same comparison on every scan, with none of that. Its headers control fetches the page, lists the inline scripts, event handlers and styles and the scripts, stylesheets, images and fonts it loads, and checks each against the policy the response carried. Here it is against a checkout page with an inline handler, a script from a CDN and a third-party font:

WHAT WILL NOT LOAD
draugr scan
onclick on <button>inline event handler
Move it into a file, or allow it by hash.
https://cdn.example.comscript · /analytics.js
Add the origin to script-src, or stop loading from it.
https://fonts.gstatic.comfont · /s/inter/v13/inter.woff2
Add the origin to font-src, or stop loading from it.

What comes back is the list of things that will not load, each with its fix, before the change merges. The run still passes, because a policy that breaks its own page is a fault to fix and exposes nobody, so these findings never fail a security gate.

A policy sent as Content-Security-Policy-Report-Only gets the same check, worded as what enforcing it would block, which is the list a team needs before switching it on. Where a policy allows 'unsafe-inline', the finding lists what on the page depends on it, or states that nothing does, and removing it stops being a guess.

The check reads the markup the server sends. A script that another script injects, or a WebSocket one opens, exists only at runtime and is out of its reach. The failures that strand a policy in report-only are inline handlers and a forgotten CDN, and those are sitting in the markup.

The headers where grading is enough

The rest of the header set is simple, and worth doing precisely because it is:

HeaderWhat it settlesWhat getting it wrong costs
Strict-Transport-Securitypresent, long enough, preferably preloadedone plaintext request is enough to intercept
X-Content-Type-Options: nosniffone value, no tradeoffan upload the browser decides to run as script
Referrer-Policyanything other than unsetyour URLs, and whatever is in them, handed to third parties
X-Frame-Options / frame-ancestorswho may frame youclickjacking

None of these interact with page content the way CSP does. They are pure configuration, they take minutes, and they are the ones a checklist settles.

Draugr checks these natively rather than shelling out, and tunes the list by whether a host is a browser surface or an API, flagging browser-only headers on a JSON endpoint is the kind of noise that teaches people to ignore a control.

The point

A checklist is the right tool for the headers that are configuration, and the wrong tool for the one that is a relationship between a policy and a page.

If your CSP has been report-only for a year, the reason is in the page, and a scan that reads the page against the policy names it.

The HTTP security headers guide covers what each header does, and DAST covers the active half of checking a running service.