Skip to content

CSP Keywords & Source Expressions

Directives say what is being restricted. Source expressions say to what. They are shared across directives, so they live here rather than being repeated on each of the directive pages.

The single most common CSP mistake is a missing pair of single quotes. 'self', 'none', 'unsafe-inline' and the rest are keywords, and the quotes are part of the syntax. Written without them they are parsed as host names, which is why script-src self silently allows nothing and reports everything.

Keyword Meaning
'self' The document’s own origin: same scheme, host and port. Does not cover data:, blob: or subdomains.
'none' Nothing at all - but only when it is the only value in the directive. See below.
'unsafe-inline' Inline scripts, inline styles and inline event handlers. Ignored when a nonce or hash is also present.
'unsafe-eval' eval(), Function(), and setTimeout/setInterval called with a string.
'wasm-unsafe-eval' WebAssembly compilation, without allowing JavaScript eval(). The narrower half of 'unsafe-eval'.
'unsafe-hashes' Lets a hash source match an inline event handler or style attribute, which a plain hash does not.
'strict-dynamic' Trust propagates from a nonced or hashed script to whatever it loads. Host allowlists are ignored.
'report-sample' Include the first characters of the offending code in violation reports.
'inline-speculation-rules' Allow inline <script type="speculationrules"> without allowing other inline script.
'nonce-<base64>' A per-response random token, matched against the nonce attribute on an element.
'sha256-<base64>' The base64 SHA-256 of an inline block’s exact contents. sha384 and sha512 are equally valid.

'none' means “no sources”, and the grammar allows it only as the entire source list. Written alongside anything else it is neither a parse error nor an override - it simply matches nothing, while the other sources go on matching:

Content-Security-Policy: default-src 'none' https://cdn.example.com

That policy allows https://cdn.example.com. A locked-down default with an exception needs the exception in its own directive:

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

'self' means the origin, and an origin is scheme, host and port together. It does not cover www.example.com when you are on example.com, it does not cover data: or blob: URLs, and it does not reliably cover wss:// for connect-src in every browser. List those explicitly.

A nonce is a random value generated per response and repeated in two places: the policy, and the element.

Content-Security-Policy: script-src 'nonce-2726c7f26c'
<script nonce="2726c7f26c">
window.APP_CONFIG = { region: "eu-west-1" };
</script>

Three rules make a nonce worth having, and breaking any of them makes it decorative:

  • It must be fresh per response. A nonce baked into a static asset is a password that every attacker can read.
  • It must be unpredictable - a cryptographically secure random value of at least 128 bits, base64-encoded.
  • It must not be placed on markup an attacker can influence. A nonce copied onto an injected <script> is a nonce that authorises the injection.

A nonce in the directive causes 'unsafe-inline' in the same directive to be ignored by any CSP Level 2 or later browser. That is deliberate, and it is how you keep a fallback for browsers that do not understand nonces.

A hash allows one exact inline block. The browser hashes the bytes between the tags and compares.

Terminal window
echo -n 'window.APP_CONFIG = { region: "eu-west-1" };' \
| openssl dgst -sha256 -binary \
| openssl enc -base64
Content-Security-Policy: script-src 'sha256-bkfRdxp5pSKxNPI1rxXSef7NbXTGd8p2EQ8u1tn0i90='

Hashes are whitespace-sensitive. Reformatting the file, or a template engine changing its indentation, invalidates the hash - which is why hashes suit generated inline blocks and nonces suit hand-written ones.

A plain hash does not cover inline event handlers or style attributes. That is what 'unsafe-hashes' is for, and it is a step on a migration rather than a destination - see script-src-attr.

'strict-dynamic' says: trust whatever an already-trusted script chooses to load. It makes host allowlists, 'self' and 'unsafe-inline' irrelevant in that directive, and it is what makes tag managers and code-split bundles workable without listing every CDN.

Content-Security-Policy: script-src 'unsafe-inline' https: 'nonce-2726c7f26c' 'strict-dynamic'

That policy is read differently by three generations of browser, which is the point:

  • A CSP 1 browser sees 'unsafe-inline' https:.
  • A CSP 2 browser sees https: 'nonce-2726c7f26c', because a nonce disables 'unsafe-inline'.
  • A CSP 3 browser sees 'nonce-2726c7f26c' 'strict-dynamic', because 'strict-dynamic' disables the host list.

Without 'report-sample' the browser sends an empty sample on inline violations. HeaderHawk groups inline issues by a hash of that sample, so a policy without it collapses every inline violation on the site into a single issue labelled unknown. Add it to script-src, script-src-attr, style-src and style-src-attr before you start triaging inline violations.

Trusted Types violations are the exception: browsers include a sample for those regardless.

A scheme source ends in a colon and allows every URL using it.

Scheme Allows
https: Any HTTPS URL, from any host. Very broad - prefer specific origins.
http: Any HTTP URL. Almost never correct on a secure site.
data: data: URIs. Needed for inlined SVGs and icon fonts; unsafe for script.
blob: blob: URLs, which bundlers and media players construct at runtime.
mediastream: MediaStream sources.
filesystem: FileSystem API URLs.
wss: Secure WebSocket URLs, for connect-src.

data: in script-src is the one to avoid outright: it lets any injected markup carry its own payload, which defeats the directive.

Pattern Matches
example.com That host, over any scheme and port.
https://example.com That host over HTTPS only.
https://example.com:443 That host, scheme and port exactly.
*.example.com Any subdomain, but not example.com itself.
https://example.com/app/ URLs under that path. A path ending in / is a prefix match.
* Any URL with a network scheme. Not data:, blob: or inline code.

Two details catch people out. *.example.com does not include the bare domain, so a policy needs both if you serve from both. And a path in a source expression is matched against the request URL, not the page’s - which makes path restrictions useful for CDNs and useless for redirect-heavy origins, since a redirect away from the allowed path is a violation.

Keyword Applies to
'unsafe-inline' script-src family, style-src family
'unsafe-eval' script-src only
'wasm-unsafe-eval' script-src only
'unsafe-hashes' script-src, script-src-attr, style-src, style-src-attr - not the -elem pair
'strict-dynamic' script-src, script-src-elem
Nonces and hashes script-src family, style-src family
'self', 'none' Every directive that takes a source list, including frame-ancestors and form-action

frame-ancestors is the strictest about this: it accepts only 'none', 'self', hosts and schemes. A nonce or 'unsafe-inline' there is a parse error’s worth of nothing.