squirrelscan
GitHub

CSP blocks your own scripts, frames and analytics endpoints

A Content-Security-Policy that leaves out a host your page depends on breaks a widget or a tracker silently. squirrel matches each page's policy against what the page loads.

What this rule catches

A Content-Security-Policy (CSP) lists the hosts a page may load scripts, frames, styles, images and network requests from. When the list leaves out a host the site depends on, the browser blocks that resource and the page still renders. A booking widget disappears, a payment frame stays empty, and analytics hits never arrive. Nothing in the HTML looks wrong, and nothing in a normal audit reports it.

security/csp judges how strict a policy is. This rule asks a different question: does the policy the page was served with allow what the page itself loads?

What squirrel checks

The rule runs on every crawled page and reads that page’s own policy, so a route with a different header is judged by its own header. It emits a check named csp-blocks-own-resources, followed by an informational csp-chunk-only-hosts note when hosts were named only inside the page’s scripts (see Third-party scripts inside your own chunks):

  • Pass when the page has no enforced policy. Message: No enforced Content-Security-Policy to check (security/csp covers a missing policy). A missing CSP stays with security/csp, and a Content-Security-Policy-Report-Only header blocks nothing, so it is not checked.
  • Pass when every resource is allowed. Message: The page's CSP allows the resources it references and the vendor hosts it uses.
  • Fail when a blocked host belongs to a vendor category the table knows: analytics, ads, payments, captcha, form or booking.
  • Warn when the blocked hosts are all something else.

The message names the directive and the hosts to add, for example The page's CSP blocks 3 hosts it depends on (connect-src: add analytics.google.com, www.google.com, stats.g.doubleclick.net). Each blocked host is listed as an item with the directive, the host, and where it was found (html or vendor).

Policies come from the Content-Security-Policy response header and from <meta http-equiv="Content-Security-Policy"> tags. When a header carries several comma-separated policies, every one of them has to allow the resource.

What the page references

SourceDirective checked
<script src>, <link rel="modulepreload">, <link rel="preload" as="script">script-src-elem, then script-src, then default-src
<link rel="stylesheet">, <link rel="preload" as="style">style-src-elem, then style-src, then default-src
<iframe src>frame-src, then child-src, then default-src
<img src>, <link rel="preload" as="image">img-src, then default-src
<link rel="preload" as="font">font-src, then default-src
<link rel="preload" as="fetch">connect-src, then default-src

Not checked: srcset, <source>, <object>, <embed>, <form action> and CSS @import. The rule only reports what it can see, so these are missed rather than guessed.

A directive the policy does not set allows everything. Elements inside <noscript> are skipped (see content inside noscript), and so are data: and blob: URLs.

Third-party scripts inside your own chunks

The audit fetches a page’s preloaded chunks only when a crawled page enforces a CSP, at most 20 extra per audit, and no other rule reads them. A bundler’s lazy chunk can name a third-party script that no HTML tag mentions. squirrel fetches the same-site scripts and the chunks a page preloads, and looks in each for a quoted https:// URL ending in .js or .mjs with script-loading code beside it (createElement("script"), .src =, import(), importScripts). If that host is not allowed by script-src, it is listed in a separate informational check, csp-chunk-only-hosts: {count} host(s) the page's CSP would block appear only as URLs inside the page's scripts, which may never load them ({directive}: {hosts}), with each item marked found: "chunk". It is a note, not a warning, because a URL in a script is not proof the page loads it: SDKs ship fallback URLs that load only behind a feature flag, such as a session replay module. A host that the HTML loads, or that a vendor the page uses needs, is still a warning, and is not repeated in the note. URLs in comments or links that nothing loads are ignored, a script a chunk injects is not listed when the policy uses nonces (the chunk may copy the page’s nonce onto it), and chunks are scanned up to 2 MB each.

Vendor hosts the HTML never names

Some hosts are only contacted at runtime, so the HTML never lists them. When the page shows a vendor in use, squirrel checks the hosts that vendor needs:

VendorDirectiveHosts
Google Analytics 4connect-srcwww.google-analytics.com, analytics.google.com. With an Ads link (AW- id), also www.google.com, stats.g.doubleclick.net
Google Adsscript-src, frame-srcgoogleads.g.doubleclick.net, www.googleadservices.com, www.google.com; frame td.doubleclick.net
Google Tag Managerscript-src, frame-srcwww.googletagmanager.com
PostHogscript-src, connect-srcus-assets.i.posthog.com or eu-assets.i.posthog.com, us.i.posthog.com or eu.i.posthog.com, by the region the page names. A first-party api_host is not checked
Stripescript-src, connect-src, frame-srcjs.stripe.com, api.stripe.com, hooks.stripe.com
Cloudflare Turnstilescript-src, frame-srcchallenges.cloudflare.com
Google reCAPTCHAscript-src, frame-srcwww.google.com, www.gstatic.com (or www.recaptcha.net)

Vendors are detected from the page’s HTML with comments removed. Vendor hosts change. The table is data in the rule, and every entry records the date it was last checked against the vendor’s own guidance. The current entries were last verified on 2026-10-08.

How sources are matched

Matching follows the CSP specification rather than a substring test:

  • A wildcard host such as *.example.com matches cdn.example.com and a.b.example.com, but not example.com. Allow both when you need both.
  • A source with no port allows only the default port for its scheme, :* allows any port, and 'self' needs the same scheme (or an https upgrade), host and port.
  • A source path ending in / is a prefix, and any other path must match exactly.
  • https: allows any https URL, and a source with no scheme follows the page’s scheme.
  • A matching 'nonce-...' on the element allows it, and a hash source ('sha256-...') on a list that allows an element with an integrity attribute is treated as undecidable, so nothing is reported.
  • 'none' or an empty list blocks everything.
  • A script-src with 'strict-dynamic' makes the browser ignore host sources, so squirrel cannot decide statically and reports nothing for scripts.

The rule is skipped on pages with no parsed document, such as error pages. Severity for the rule is warning, weight 6.

How to fix it

Add each reported host to the directive the finding names, then reload the page with the browser console open and confirm no CSP violation remains.

Content-Security-Policy: default-src 'self';
  script-src 'self' https://www.googletagmanager.com;
  connect-src 'self' https://www.google-analytics.com https://analytics.google.com https://stats.g.doubleclick.net https://www.google.com

Test a change in Content-Security-Policy-Report-Only first if the site is busy. If a blocked host is something you no longer use, remove the tag that loads it instead.

Rule IDsecurity/csp-blocks-own-resources
CategorySecurity
ScopePer-page
Severitywarning
Weight6/10

Enable / disable

Disable this rule

[rules]
disable = ["security/csp-blocks-own-resources"]

Disable all Security rules

[rules]
disable = ["security/*"]

Enable only this rule

[rules]
enable = ["security/csp-blocks-own-resources"]

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.

Type to search…

↑↓ navigate↵ openesc close