CSP 'Refused to load': why default-src does not cover script-src
· csp · security-headers · browser · xss · http-headers
cspsecurity-headersbrowserxsshttp-headersA 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.