squirrelscan
GitHub

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:

  • csp skipped when the crawl produced no pages. Message: No pages to check.
  • csp-missing warns when neither Content-Security-Policy nor Content-Security-Policy-Report-Only is present. Message: No Content-Security-Policy header. No further checks run.
  • csp-report-only warns when only the report-only header is set. Message: CSP in report-only mode (not enforced).
  • csp-present passes 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-control warns when the policy declares neither script-src nor default-src. Message: CSP has no script-src or default-src directive.
  • csp-unsafe-scripts warns 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 with and.
  • csp-wildcard warns 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 as https://*.example.com is scoped to one domain’s subdomains and does not trigger it. Message: CSP script-src allows wildcard (*).
  • csp-no-frame-ancestors reports info when frame-ancestors is absent and no X-Frame-Options header is set either. Message: CSP missing frame-ancestors directive.
  • csp-no-object-src reports info when neither object-src nor default-src is 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 IDsecurity/csp
CategorySecurity
ScopeSite-wide
Severitywarning
Weight6/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.

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.

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

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.

Type to search…

↑↓ navigate↵ openesc close