Dr.Who
← blog

CSP 'Refused to load': why default-src does not cover script-src

· csp · security-headers · browser · xss · http-headers

cspsecurity-headersbrowserxsshttp-headers

A Refused to load ... because it violates the Content-Security-Policy directive error means the browser blocked a resource because the directive that governs it does not list that source. The trap most people hit: default-src is only a fallback. The moment you declare a specific fetch directive like script-src, that directive no longer inherits anything from default-src — so a host you allowed in default-src but forgot to repeat in script-src is still blocked.

Reading the violation message

The error always names the exact directive and the blocked URL. Read it literally:

Refused to load the script 'https://cdn.example.com/a.js'
because it violates the following Content-Security-Policy directive:
"script-src 'self'".

That tells you everything: the resource type (script), the URL (https://cdn.example.com/a.js), and the directive that rejected it (script-src 'self'). The browser is not guessing — it picked script-src because the resource was a script, then found cdn.example.com was not in the allow-list. The fix is always to add the named source to the named directive, not to default-src.

Why default-src is a trap

Say you publish this header:

Content-Security-Policy: default-src 'self' https://cdn.example.com; script-src 'self'

A stylesheet or image from cdn.example.com loads fine — those have no dedicated directive, so they fall back to default-src, which allows the CDN. But a script from cdn.example.com is refused, because script-src is set explicitly and lists only 'self'. Specifying a fetch directive replaces the fallback for that resource type; it does not merge with it. Either repeat the source in every relevant directive, or rely on default-src and don't declare the narrower one.

Some directives never fall back

Even removing all your fetch directives won't save these — they ignore default-src entirely:

  • frame-ancestors — who may embed you in a frame (clickjacking control).
  • base-uri — what <base href> values are allowed.
  • form-action — where forms may submit.
  • report-uri / report-to — where violation reports are sent.

These are document and navigation directives, not fetch directives. If you rely on default-src to lock down framing, you are not protected — you must set frame-ancestors 'none' (or your allowed origins) directly.

Fixing inline scripts the right way

A common cause is an inline <script> blocked by script-src 'self'. Reaching for 'unsafe-inline' defeats most of CSP's XSS value. Prefer a nonce or hash:

Content-Security-Policy: script-src 'self' 'nonce-r4nd0m'
<script nonce="r4nd0m">/* allowed */</script>

Generate a fresh nonce per response and stamp it on every inline script you control. For static inline blocks, a 'sha256-...' hash of the script body works without server-side randomness. Either path lets you keep 'unsafe-inline' out of the policy.

Scan your security headers and CSP →

Further reading

  • The security headers guide
  • The CSP spec itself is the W3C Content Security Policy Level 3 recommendation, which defines every directive and its fallback behavior.