Article

CSP script-src and default-src: The Fallback Chain, Wildcards and a Starter Policy

default-src is the fallback for every CSP fetch directive you don't set; script-src decides whether injected code runs. Write script-src explicitly with exact origins — never * or a bare https: — and add object-src, base-uri, form-action and frame-ancestors, which default-src does not cover.

By Paul Rudenko, Security ResearcherUpdated Sep 18, 20268 min read

default-src is the fallback for every fetch directive you don't set; script-src is the one that decides whether injected code runs. A policy without either doesn't restrict scripts at all. Write script-src explicitly, name exact origins (never * or a bare https:), and add the four directives that default-src does not cover: object-src, base-uri, form-action, frame-ancestors.

How does the CSP fallback chain work?

When the browser needs to load a resource, it looks for the most specific directive and walks up a defined chain until it finds one that's set. Only the first match applies — the chain is not cumulative (CSP Level 3, directive fallback list):

script-src-elem  →  script-src  →  default-src      (external <script> and inline blocks)
script-src-attr  →  script-src  →  default-src      (onclick="…" handlers)
style-src-elem   →  style-src   →  default-src
worker-src       →  child-src   →  script-src  →  default-src
frame-src        →  child-src   →  default-src
img-src / font-src / connect-src / media-src / object-src / manifest-src  →  default-src

NO fallback (must be written explicitly or they simply don't exist):
base-uri · form-action · frame-ancestors · sandbox · report-to

Two practical consequences. First, default-src 'self' alone is a real policy — scripts, images and connections are all limited to your origin. Second, the moment you write script-src, default-src stops applying to scripts entirely: if default-src had a nonce and script-src doesn't, the nonce is gone for scripts. The CSP Checker resolves this chain the way the browser does and tells you which directive actually governed scripts when it reports a weakness.

What belongs in script-src?

  • 'self' — scripts from your own origin (same scheme, host and port).
  • Exact third-party originshttps://js.stripe.com, https://www.googletagmanager.com. Scheme + host; a path is allowed but rarely worth the brittleness.
  • 'nonce-…' or 'sha256-…' — for inline scripts; see replacing 'unsafe-inline'.
  • 'strict-dynamic' — with a nonce/hash, so trusted scripts may load further scripts without you listing every CDN. Without a nonce or hash it does nothing useful; the checker flags that combination.

What does not belong there, and why the checker grades it HIGH:

  • *, https:, http: — “any host”. An attacker who can inject a <script src="https://evil.example/x.js"> is fully allowed.
  • data: and blob: — let injected markup smuggle a script as a data URL. Fine for img-src; not for scripts.
  • Broad CDN hosts that serve anything anyone uploads (public JS CDNs, storage buckets, some cloud-function domains). If the host also serves attacker-controllable files, listing it is equivalent to * — Google's CSP research called these “JSONP and Angular endpoints on whitelisted hosts” the most common bypass (Weichselbaum et al.).

Which four directives does default-src not cover?

These are the “missing directive” notes the checker adds even to an otherwise good policy. They are informational because they are defence-in-depth, but each is one line and there is no reason to skip them:

  • object-src 'none' — blocks <object>/<embed> plugin content, a historical script-execution path. No modern site needs plugins.
  • base-uri 'self' — stops an injected <base href> from redirecting every relative script URL on the page to an attacker's host.
  • form-action 'self' — stops an injected form from posting user input (passwords, card numbers) to another origin. Add your payment provider's origin if you post directly to it.
  • frame-ancestors 'none' — who may put your page in an iframe (clickjacking). Separate guide: frame-ancestors.
Common gotcha: default-src 'none' is a fine starting point for an API or a static page, but on a normal site it blocks everything you didn't list — including same-origin images, fonts and the manifest. Start from default-src 'self' and tighten individual directives instead of starting from nothing and discovering the gaps in production.

For the site owner and for the developer

For the site owner (plain English)

A Content-Security-Policy is a list of places your website is allowed to load code from. If a report says the list “allows any host” or “doesn't restrict scripts”, the list exists but has a wildcard in it — like a guest list that says “everyone”. The fix is for whoever runs the site to replace the wildcard with the actual handful of services you use (your own domain, your analytics, your payment provider). The checker can draft that list for you from what the homepage already loads.

Paste a policy or enter your domain — the checker resolves the fallback chain, names the directive that governs scripts, and hands back your policy with the four hardening directives appended.

Check and harden your policy

For the developer (exact syntax)

A starter policy for a typical marketing site with analytics and one payment provider:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-{RANDOM}' 'strict-dynamic' https://www.googletagmanager.com https://js.stripe.com;
  style-src 'self' 'unsafe-inline';                 # styles: low risk, fix later with nonces
  img-src 'self' data: https://www.google-analytics.com;
  font-src 'self';
  connect-src 'self' https://www.google-analytics.com https://api.stripe.com;
  frame-src https://js.stripe.com;
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';
  report-to csp-endpoint

Generate a policy from what a page actually loads (a quick first draft, not a substitute for the report-only pass):

# List the external origins a page references in its HTML
curl -s https://example.com/ \
  | grep -oE '(src|href)="https?://[^/"]+' \
  | sed -E 's/.*="//' | sort | uniq -c | sort -rn
# nginx
add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://js.stripe.com; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always;

# Apache
Header always set Content-Security-Policy "default-src 'self'; script-src 'self' https://js.stripe.com; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'"

Deploy it as Report-Only first; the violation reports are the definitive list of origins you forgot.

Frequently asked questions

What is the difference between default-src and script-src in CSP?

default-src is a catch-all: for any fetch directive you do not write — scripts, styles, images, fonts, connections, frames, media, workers — the browser uses default-src's source list instead. script-src is the specific directive for JavaScript, and it is the one that matters most for security because it decides whether an injected script can execute. The two interact through the fallback chain: if script-src is absent, default-src governs scripts; the moment script-src is present, default-src no longer applies to scripts at all, even if default-src was stricter. In practice you write default-src 'self' as the baseline and then a deliberate script-src with your nonce, 'strict-dynamic' and the exact third-party origins you load code from.

Is script-src * or script-src https: a security problem?

Yes — it is the single most common way a Content-Security-Policy ends up providing no script protection while looking like it does. A wildcard or a bare scheme such as https: allows scripts from any host on the internet, so an attacker who can inject a script tag pointing at their own server is fully permitted by the policy. The same applies to data: and blob: in script-src, which let injected markup carry the script inline as a URL. Our checker grades these as a HIGH finding and caps the policy at C. Replace them with the exact origins you use, or better, move to a nonce plus 'strict-dynamic' so you don't need a host list at all.

Why does my CSP checker say object-src, base-uri or form-action is missing when I have default-src?

Because those directives are not fetch directives and are not covered by default-src, so if you don't write them they don't exist. object-src actually is a fetch directive and does fall back to default-src, but both Google's and OWASP's guidance is to set object-src 'none' explicitly because default-src 'self' would still allow same-origin plugin content. base-uri controls the <base> tag, form-action controls where forms may submit, and frame-ancestors controls who may iframe your page; none of them inherit from default-src. Each is a single, low-risk line — object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none' — which is why checkers report them as informational hardening rather than as weaknesses.

Can a CSP generator create a safe policy for my site automatically?

A generator can produce a good first draft by reading the origins your pages reference — scripts, stylesheets, images, frames and form targets — and turning them into directives. That draft is only as complete as the pages it saw: resources loaded dynamically by JavaScript, pages behind login, and A/B-tested variants won't appear in a static read. Treat the generated policy as the starting point for a Report-Only rollout, not as a finished header. Deploy it with report-to, collect violation reports for a week of real traffic, add the origins you missed, then enforce. Our CSP Checker returns your pasted policy with the missing hardening directives appended and, in URL mode, a starter policy built from the homepage's observed origins.

Related guides

See your whole external attack surface

One page is a start. The full external scan covers TLS, headers, DNS, exposed files, open services and known-exploited CVEs across your whole domain.

See the full scan →