Content-Security-Policy: what to set and how to check it
A missing CSP, a report-only policy or an unsafe-inline script-src leaves XSS unmitigated. squirrel parses the header and names each gap.
What a Content Security Policy is
Content-Security-Policy is a response header that lists, per resource type, which origins a page is allowed to load from and whether inline code may run. The browser enforces it and blocks anything outside the policy. Its main job is to contain cross-site scripting: an injected <script> cannot execute if the policy has no matching source.
A policy is only worth what its script-src says. 'unsafe-inline' re-permits exactly the injected code CSP exists to stop, unless the same source list also carries a nonce or a hash, in which case browsers ignore it. A wildcard source lets any host serve executable code. Content-Security-Policy-Report-Only sends violation reports without blocking anything, which is the right way to develop a policy and the wrong way to ship one.
What squirrel checks
The rule reads the response headers of the first crawled page and can emit several checks:
cspskipped when the crawl produced no pages. Message:No pages to check.csp-missingwarns when neitherContent-Security-PolicynorContent-Security-Policy-Report-Onlyis present. Message:No Content-Security-Policy header. No further checks run.csp-report-onlywarns when only the report-only header is set. Message:CSP in report-only mode (not enforced).csp-presentpasses when the enforcing header is set. Message:CSP header present and enforced, with the first 100 characters of the policy as the value.csp-no-script-controlwarns when the policy declares neitherscript-srcnordefault-src. Message:CSP has no script-src or default-src directive.csp-unsafe-scriptswarns when the effective script source lists'unsafe-eval', or lists'unsafe-inline'with no nonce, hash or'strict-dynamic'beside it (see below). Message:CSP allows <keywords>, where<keywords>is'unsafe-inline','unsafe-eval'or both joined withand.csp-wildcardwarns when that same source list contains a source whose host is a bare*, such as*,https://*or*:443, which lets scripts load from any host, and the list has no'strict-dynamic'. A host wildcard such ashttps://*.example.comis scoped to one domain’s subdomains and does not trigger it. Message:CSP script-src allows wildcard (*).csp-no-frame-ancestorsreports info whenframe-ancestorsis absent and noX-Frame-Optionsheader is set either. Message:CSP missing frame-ancestors directive.csp-no-object-srcreports info when neitherobject-srcnordefault-srcis declared. Message:CSP missing object-src directive.
The effective script source is script-src when it is present and non-empty, otherwise default-src. Its sources are compared as whole tokens, ignoring case, the way browsers read them: 'UNSAFE-INLINE' is 'unsafe-inline', and a host that merely contains one of these strings is not a match. A header that carries several comma-separated policies is read as one list. Severity is warning and the scope is site-wide.
Some sources are ignored by browsers when the same list carries others (CSP Level 3), and squirrel ignores them too:
- A nonce (
'nonce-…') or a hash ('sha256-…','sha384-…','sha512-…') makes'unsafe-inline'ignored. 'strict-dynamic'makes'unsafe-inline','self'and every host and scheme source ignored, a bare*included.- Neither changes
'unsafe-eval', which always warns.
So a strict policy of the shape Google recommends, such as script-src 'nonce-r4nd0m' 'strict-dynamic' https: 'unsafe-inline', passes: https: and 'unsafe-inline' are only there as a fallback for browsers older than CSP Level 3. Only the directive’s own list counts, so a nonce after a comma, in a second policy’s other directive, does not switch a warning off.
How to fix it
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-r4nd0m'; object-src 'none'; frame-ancestors 'self'
Set the header at the edge or in the application response, and generate a fresh nonce per response for the inline scripts you cannot remove. Develop the policy under Content-Security-Policy-Report-Only first, then move the same value to the enforcing header.
A policy that legitimately allows a wildcard for images or fonts is not flagged. Only the script source list is checked for *, 'unsafe-inline' and 'unsafe-eval'.
| Rule ID | security/csp |
| Category | Security |
| Scope | Site-wide |
| Severity | warning |
| Weight | 6/10 |
Enable / disable
Disable this rule
[rules]
disable = ["security/csp"]Disable all Security rules
[rules]
disable = ["security/*"]Enable only this rule
[rules]
enable = ["security/csp"]enable replaces the default ["*"], so this is the only rule selected. Leave disable out: a rule that matches any disable pattern is skipped even when enable names it. Only an enabled setting under a rule’s own [rule_options] table overrides both lists.
Related rules
- security/x-frame-options: the older header that
frame-ancestorsreplaces. - security/sri: per-file hashes for the third-party origins your policy allows.
- security/permissions-policy: the companion header for device and browser features.
- security/leaked-secrets: what an attacker collects once a script does run.
Security findings ship in every audit next to the SEO, performance and agent experience rules. See Website security scan with AI for how an agent works through a report.
Related rules
security/csp-blocks-own-resources checks the opposite failure: a policy that is present but leaves out a host the page loads, so the browser blocks it.
References
- Content-Security-Policy, MDN
- Content Security Policy Level 3, W3C
- Mitigate cross-site scripting with a strict CSP, web.dev
Check your site
Run squirrel audit https://example.com and open the Security section of the report. The policy is parsed and each weak directive is named. Signed-out local audits need no account and cost nothing. Signed-in audits spend credits.