After a site enables a Content-Security-Policy with script-src 'self', the browser console reports that the page refused to evaluate a string as JavaScript. Which code patterns cause that, and what are the options for fixing them?
answer
- strings compiled at runtime
- not just the obvious call
- two timer functions accept a body
- the polyfill hunting for the global
- dev source maps wrap modules
basics
~20 sCSP blocks turning strings into code: eval(), the Function constructor, and setTimeout or setInterval called with a string body. Fix by rewriting those call sites — usually a runtime template compiler or a dev build setting — rather than adding 'unsafe-eval'.
solid answer
~50 sCSP blocks the string-to-code path unless `'unsafe-eval'` is present. Four patterns trigger it: `eval()`, `new Function(...)`, and `setTimeout`/`setInterval` given a string instead of a function. In real apps the offender is rarely your own `eval` call — it is usually a runtime template or expression compiler that builds a function from a string, a polyfill doing `Function('return this')()` to find the global, or a development bundle whose source-map setting wraps each module in an `eval`. The fixes in order of preference: move template compilation to build time, replace global-detection with `globalThis`, and switch the dev build to a non-eval source-map mode so the policy behaves the same in every environment. Adding `'unsafe-eval'` is the last resort, and worth resisting: it turns any gadget that feeds attacker-controlled text into `eval` into script execution. WebAssembly compilation is gated separately in CSP Level 3 by `'wasm-unsafe-eval'`.
code
javascript · 11 lines// Blocked without 'unsafe-eval'
eval('total = a + b');
const add = new Function('a', 'b', 'return a + b');
const globalObj = Function('return this')();
setTimeout('refresh()', 1000);
// CSP-clean equivalents
const total = a + b;
const addFn = (a, b) => a + b;
const globalObj2 = globalThis;
setTimeout(() => refresh(), 1000);go deeper
Be able to list the blocked patterns on demand — eval, the Function constructor, and setTimeout or setInterval given a string — and say that passing a function instead is fine.
Explain where these come from in a real bundle: runtime template compilers, Function('return this') polyfills, and eval-based dev source maps, and how you would locate the offender.
Argue the fix hierarchy convincingly — precompile, rewrite the idiom, fix the build config, loosen only one route — and articulate why 'unsafe-eval' turns dependency data flows into execution sinks.
Own the org-level position: whether 'unsafe-eval' is ever grantable, what evidence a team must bring to request it, and how libraries needing runtime compilation are handled in the platform's build.
## What CSP is actually blocking One of CSP's two core jobs is to close the paths by which a string becomes executable code. (The other is controlling which URLs may be loaded.) Unless `script-src` contains `'unsafe-eval'`, the browser refuses these: ```js eval('doThing()'); // blocked new Function('a', 'return a + 1'); // blocked Function('return this')(); // blocked — same constructor, no new setTimeout('doThing()', 0); // blocked — string argument setInterval('tick()', 1000); // blocked — string argument ``` And these are fine, because no string is being compiled: ```js setTimeout(() => doThing(), 0); JSON.parse(text); new Worker('/worker.js'); // governed by URL rules, not string evaluation ``` The distinction is mechanical: does the call hand the JavaScript engine source text to compile at runtime? A callback is already compiled; a string is not. ## Where it comes from in real code Almost nobody writes `eval` deliberately any more, yet this is the single most common first casualty of enabling CSP. The usual sources: - **Runtime template or expression compilers.** Any library that takes a template string or a user-authored expression and builds a function from it uses `new Function` internally. Client-side templating engines, spreadsheet-style formula evaluators, rules engines and query-filter builders all do this. - **Global-object detection in polyfills.** The historical portable way to obtain the global object was `Function('return this')()`. Older polyfill bundles still contain it, and it fires on the very first line of your vendor chunk. - **Development bundles.** Some bundler source-map settings wrap every module body in an `eval` call to get cheap per-module mapping — webpack's `eval`, `eval-source-map` and `eval-cheap-source-map` devtool values do exactly this. The result is a policy that passes in production and fails in local development, which teams often resolve by loosening the policy for everyone instead of changing the dev setting. - **Legacy JSON handling** in very old code that evaluates a response body instead of parsing it — which is also a security defect in its own right. ## Fixing it Rank the options: 1. **Move compilation to build time.** If a templating library offers a precompile step, use it: the build emits real functions and the runtime compiler never ships. This removes the requirement rather than accommodating it. 2. **Replace the specific idiom.** `Function('return this')()` becomes `globalThis`. A hand-rolled expression evaluator becomes a real parser that walks a syntax tree and interprets it — an interpreter never compiles source, so CSP has nothing to block. 3. **Change the build configuration.** Pick a non-eval source-map mode for whatever environment runs under the policy, so development and production behave identically. A policy that only holds in production is a policy nobody trusts. 4. **Loosen the policy — last, and narrowly.** A policy is attached per response, so if exactly one legacy route genuinely needs it, that route can be served a looser policy while the rest of the site stays strict. ## Why `'unsafe-eval'` is worth resisting It is tempting to treat it as harmless because it does not, by itself, let an attacker load a script. The harm is indirect and real: with `'unsafe-eval'`, any place in your code that passes attacker-influenced text into `eval` or `new Function` becomes full script execution inside your origin. Those places are exactly the ones you cannot audit — inside dependencies, inside a template compiler processing a value from the URL, inside a rules engine fed by API data. The keyword converts a whole class of latent data-flow bugs into live vulnerabilities, which is why hardening guidance treats it as roughly as serious as `'unsafe-inline'`. ## WebAssembly Compiling WebAssembly from bytes is a separate capability. CSP Level 3 defines `'wasm-unsafe-eval'` so a page can permit `WebAssembly.compile` and `WebAssembly.instantiate` without granting general JavaScript string evaluation. If a wasm-backed library breaks under your policy, that keyword — not `'unsafe-eval'` — is the correct minimal grant. ## Diagnosing quickly The violation report names the directive but not always a useful location, since the offending frame is inside a bundled dependency. The practical route is to reproduce with the unminified vendor bundle and search it for `new Function`, `Function(` and `eval(`; between them these three strings find nearly every case.
- Is setTimeout(() => doThing(), 0) affected by the same restriction?No. The arrow function is already compiled code, so nothing is being evaluated from a string and CSP has no reason to intervene. Only the string overload — `setTimeout("doThing()", 0)` — routes through the same evaluation path as `eval`, which is why it is blocked alongside it.
- Why is adding 'unsafe-eval' considered close to as damaging as 'unsafe-inline'?Because it re-opens string execution for code you did not write. Any dependency that pipes a value from the URL, an API response or user input into a template compiler becomes a script-execution sink. You cannot audit that surface, so the keyword converts latent data-flow bugs across the whole dependency tree into live vulnerabilities.
- A WebAssembly library fails to instantiate under your policy. What is the minimal fix?Add `'wasm-unsafe-eval'` to `script-src`. Defined in CSP Level 3, it permits `WebAssembly.compile` and `WebAssembly.instantiate` without granting JavaScript string evaluation, so the blast radius stays confined to wasm compilation instead of re-enabling `eval` and the Function constructor for every dependency.
saying these in an interview costs you the question
- Adds 'unsafe-eval' as the first response to the error
- Thinks only literal eval() calls are affected
- Believes JSON.parse is blocked by CSP
- Misses setTimeout called with a string body
- Loosens production policy to fix a dev-build source-map setting