Why can `'unsafe-eval'` in a Content-Security-Policy not be scoped to the one legacy widget that needs it?
answer
- a capability, not a source
- no URL at the moment of the check
- nothing for a host to match
- read from script-src or its fallback
- document-wide, never per call site
basics
~20 s'unsafe-eval' is a document-wide capability switch rather than a source. The check fires when a string is compiled into code, where there is no URL, no element and no origin to key a narrower grant on.
solid answer
~50 s`'unsafe-eval'` sits in a source list but describes no source: nothing is ever matched against it. It answers one question, asked at a moment that has nothing to do with a fetch - may this document turn a string into executable code? At that moment there is no URL and no element to attribute the request to, so there is nothing a `host-source` could narrow and nothing a nonce could be attached to. The keyword is read from `script-src`, or from `default-src` when `script-src` is absent, and the narrower script directives are not consulted for it. The grammar has no per-call, per-library or per-origin form, so the widget's requirement is the whole document's capability - including for any attacker-controlled data that reaches a compilation path. Narrowing it means changing those call sites, which is a code change, not a policy change.
code
pseudocode · 11 lineson request to compile a string into code:
let list = source list of script-src
if script-src is absent:
let list = source list of default-src
if list contains 'unsafe-eval':
compile and run
else:
refuse and report a violation
// no URL, no element, no origin is available herego deeper
Remember the shape: the keyword is about whether the document may turn strings into code at all, and it is either present for the whole document or absent. It is not about which host script comes from.
Explain why no scoping exists: at the moment of the check there is no URL, no element and no stable content, so none of the narrowing mechanisms a source list offers has anything to match on.
In review, state the consequence rather than the rule: one legacy component's requirement is a document-wide capability, and removing it is a change to those call sites, not a line edit in the policy.
The judgment call is whether the component earns a document-wide capability at all, or whether the work to retire that call site is cheaper than carrying the exception across every page that ships the policy.
## A capability, not a source Every other entry in a `serialized-source-list` is something a URL can be compared against: a `host-source`, a `scheme-source`, `'self'`, a `nonce-source`, a `hash-source`. `'unsafe-eval'` is a `keyword-source` that nothing is ever compared against. It exists to answer a single yes/no question that a conforming browser asks at a completely different moment from a fetch: **may this document turn a string into executable code?** That framing is the whole answer to the interview question. Scoping is only possible where there is something to key the scope on. ## When the check actually runs 1. Some code path already running in the document hands the JavaScript engine a string and asks for it to be compiled. 2. Before compiling, the browser consults the document's policy. 3. It reads `script-src`; when the policy carries no `script-src`, `default-src` stands in as the fallback. 4. If that source list contains `'unsafe-eval'`, compilation proceeds. If not, the compilation is refused and a violation is reported. Note what is absent from every one of those steps: **a URL, an element, an origin, a file**. The string being compiled is text already inside the document. It has no address, so there is no address to allow. ## Why there is nothing to scope it to Every narrowing mechanism a Content-Security-Policy offers is keyed on something the browser can observe at the moment of the check: - A `host-source` narrows by origin, because a fetch carries a URL. - A `path-part` narrows further, because that URL carries a path. - A `nonce-source` narrows by element, because an element carries an attribute the author set. - A `hash-source` narrows by content, because inline content has exact bytes to digest. String compilation has none of these. There is no URL, no element, and no stable content - the string is usually built at runtime. The narrower script directives introduced later do not help either: the keyword is read from `script-src`, or from `default-src` as its fallback, and the finer-grained script directives are not consulted for this check at all. So the grammar offers no per-call, per-library or per-origin form of `'unsafe-eval'`, because there is nothing for such a form to match on. ## The two unsafe keywords are independent | | `'unsafe-inline'` | `'unsafe-eval'` | |---|---|---| | What it permits | inline element text, handler attributes, `javascript:` targets | compiling a string into code | | What the grant is keyed on | the type of inline position | nothing - the whole document | | Where it is read from | the inline check for that type's list | `script-src`, or `default-src` as fallback | | Effect of a nonce or hash in the same list | the keyword is ignored | no effect at all | The last row is the one that trips people. A policy can be carefully nonce-based, with `'unsafe-inline'` sitting in the list doing nothing, and still hand the document a blanket string-compilation capability because `'unsafe-eval'` is in the same list and nothing overrides it. ## What it means for an audit of the statement viewer - **One widget's requirement is the document's capability.** The legacy component asked for it; every other script in the page, and every string that reaches a compilation path, gets it too. - **It grants the attacker nothing directly.** An injected `<script>` element is not permitted by it, and neither is a `javascript:` link - those need the other keyword. What it grants is that *if* attacker-controlled data reaches a path that compiles a string, that data becomes code. - **Its presence says nothing about inline.** The two keywords are read by different checks; finding one tells you nothing about the other. - **Its absence is not a guarantee either.** A page with no compilation capability can still execute injected script through an inline position or a permissive source. - **The only narrowing available is at the call sites.** Because the capability is document-wide, moving the widget off string compilation is the only thing that lets the keyword come out - and that is a change to code somebody wrote, not a change to the policy. ## The reading to take away When a policy is presented as "strict, with one exception for a legacy widget", `'unsafe-eval'` is not an exception with a boundary around it. It is a property of the whole document, granted for as long as the keyword is in the list, to every piece of code the document runs - the page's own, anything loaded from an allowlisted host, and anything an injection manages to route into a compilation path.
- If the policy is nonce-based, does the nonce limit what `'unsafe-eval'` applies to?No. A `nonce-source` or `hash-source` in the same list causes `'unsafe-inline'` to be ignored, but it has no effect on `'unsafe-eval'`: a nonce is keyed on an element, and string compilation has no element. The capability applies to every script the document runs, nonced or not.
- Does `'unsafe-eval'` let an attacker who can inject markup execute script directly?Not by itself. Injecting a `<script>` element or a `javascript:` link needs the inline keyword or a matching source. What `'unsafe-eval'` adds is a second route: any attacker-controlled data that reaches a path in the application which compiles a string becomes code, with no injected markup required at all.
- Two policies arrive on the response and only one carries `'unsafe-eval'`. What happens?Each policy is enforced on its own terms, so a compilation has to be permitted by both; the policy without the keyword refuses it. Adding a second, looser policy never widens what an existing one allows.
saying these in an interview costs you the question
- Thinks the keyword can be limited to one script or host
- Says a nonce narrows which code may compile strings
- Believes it also permits inline script elements
- Calls it a source expression that matches a URL
- Assumes the narrower script directives can carry it