All posts

What unsafe-eval does in CSP and how to remove it

CentralCSP Team ·

Last update:

If your Content Security Policy (CSP) contains 'unsafe-eval', you are letting the page turn arbitrary strings into running code. CSP is an HTTP response header that tells the browser which scripts it is allowed to run, and by default it blocks the functions that compile a string into executable JavaScript. The 'unsafe-eval' keyword switches that block back off.

The short version: do not ship 'unsafe-eval' in a script directive. Find what depends on it (usually a framework feature or an injected snippet), move to a build that does not need runtime code compilation, and if you only need WebAssembly, use the narrower 'wasm-unsafe-eval' instead. The rest of this post explains what the keyword allows, why it weakens the policy, where it tends to sneak in, and how to migrate off it with Report-Only first.

New to CSP? Get started with Content Security Policy covers the basics first. For the related inline-script problem, see why you should never use unsafe-inline in CSP.

What does 'unsafe-eval' do?

By default, a CSP that sets script-src blocks every API that compiles a string into code. Adding 'unsafe-eval' to that directive re-enables them. The functions it unblocks are:

  • eval("..."), which runs a string as JavaScript.
  • The Function constructor, new Function("a", "return a + 1"), which builds a function from string source.
  • setTimeout("code", 0) and setInterval("code", 0) when the first argument is a string rather than a function.
  • Compiling and instantiating WebAssembly, which historically also required 'unsafe-eval'.

When 'unsafe-eval' is absent, the browser throws on each of these and reports a violation. Passing a function to setTimeout keeps working; only the string form is affected.

Why it weakens the policy

The whole point of a strict CSP is that the browser runs only code you marked as trusted, through a nonce or a hash. String-to-code APIs route around that. Once eval is allowed, any attacker-controlled string that reaches an eval sink becomes running code, which is exactly the foothold most cross-site scripting (XSS) attacks want.

A real-world chain looks like this: user input lands in a value that some library later passes to eval or new Function (a template compiler, an expression evaluator, a config parser). Without 'unsafe-eval' the browser refuses to compile it. With 'unsafe-eval' present, the injected string runs with full page privileges, and the nonce or hash protection you set up does nothing to stop it.

CSP is defense in depth, not a replacement for input handling. You still validate and escape. 'unsafe-eval' removes the layer that contains the damage when something slips through.

Where 'unsafe-eval' sneaks in

Most teams do not add 'unsafe-eval' on purpose. It creeps in because a tool needs it:

  • Front-end frameworks in JIT mode. Angular with the just-in-time compiler compiles templates at runtime through code generation. Vue with the runtime compiler build (templates compiled in the browser) does the same. Both need 'unsafe-eval' only in those modes.
  • Tag managers with custom JavaScript. Google Tag Manager custom HTML and custom JavaScript variables evaluate strings, which pulls in 'unsafe-eval'. See running Google Analytics and Tag Manager under a strict CSP for the workarounds.
  • Older bundlers and dev builds. Some development builds and source-map setups use eval to wrap modules. Production builds usually do not, so this is often a dev-only leak that should never reach production headers.
  • Charting, expression, and templating libraries. A library that lets users write formulas or templates may compile them with new Function. Check the library before allowing eval for it.
  • WebAssembly. Compiling a .wasm module used to count as an eval for CSP purposes, so any WASM-using code dragged in 'unsafe-eval'.

How to remove it

The fix is almost always to stop compiling code at runtime, not to keep the keyword. If you cannot remove an eval sink yet, enable Trusted Types so the remaining string compilation has to pass through a policy first.

Switch frameworks to ahead-of-time (AOT) builds

Angular's default production build is AOT: templates are compiled during the build, so the browser never compiles anything and 'unsafe-eval' is not needed. Make sure you are not shipping a JIT build to production. For Vue, use the runtime-only build and precompile templates with the build tooling rather than the runtime compiler.

Content-Security-Policy: script-src 'self' 'nonce-r4nd0m' 'strict-dynamic'

That is the target: a script directive with no 'unsafe-eval' at all, trusting scripts through a nonce and 'strict-dynamic'. For how that keyword propagates trust, see the strict-dynamic guide.

Replace eval-based code

For your own code, the string forms have direct replacements:

// Before: needs 'unsafe-eval'
setTimeout("doWork()", 1000); // [!code --]
const add = new Function("a", "b", "return a + b"); // [!code --]

// After: works under a strict policy, no 'unsafe-eval'
setTimeout(() => doWork(), 1000); // [!code ++]
const add = (a, b) => a + b; // [!code ++]

Pass a function to setTimeout and setInterval. Replace new Function with a real function or a small lookup table of known operations. Parse JSON config with JSON.parse, never eval.

Move template or expression evaluation to build time

If a library compiles templates or expressions in the browser, switch to its precompiled mode or precompute the values during your build, so nothing reaches a runtime eval sink.

The narrower option, 'wasm-unsafe-eval'

If the only reason you needed 'unsafe-eval' was WebAssembly, there is a much smaller keyword. 'wasm-unsafe-eval' allows compiling and instantiating WASM modules but does not re-enable eval, new Function, or string timers. It is the right choice when you ship a WASM module and want everything else to stay blocked.

Content-Security-Policy: script-src 'self' 'wasm-unsafe-eval'

Use 'wasm-unsafe-eval' instead of 'unsafe-eval' whenever WASM is your only need. It is widely supported in current browsers. 'unsafe-eval' does permit WebAssembly compilation, but it also re-enables eval, new Function, and string timers, so using it just for WASM is wider than necessary. 'wasm-unsafe-eval' is the narrower keyword that allows only WASM compilation and leaves everything else blocked.

Violation rows whose blocked origin reads eval rather than a host

A safe migration path off 'unsafe-eval'

Removing 'unsafe-eval' can break anything that quietly relied on it, so move in stages and watch reports before you enforce. This is the same Report-Only-first approach as enforce vs Report-Only mode.

  1. Find the eval usage. Search your bundle and dependencies for eval(, new Function(, and string setTimeout/setInterval. The CSP evaluator flags when a live policy still carries 'unsafe-eval'.
  2. Fix each case. Move frameworks to AOT, replace string timers and new Function, and switch WASM to 'wasm-unsafe-eval'.
  3. Test in Report-Only. Ship the policy without 'unsafe-eval' on the Content-Security-Policy-Report-Only header. The browser blocks nothing, it only reports what it would block, so you see every remaining eval sink on real traffic.
  4. Enforce. When Report-Only is quiet, move the same policy to the enforcing Content-Security-Policy header.

CentralCSP collects those Report-Only reports for you, groups them, and shows the scripts running on each page, so you can pinpoint which dependency is still calling eval before you flip to enforce. That is the core of the CSP suite. You can start a free trial and point a Report-Only header at it to watch the migration.

For the full directive and keyword reference, see the CSP policy reference.

Frequently asked questions

Is 'unsafe-eval' safe to keep?

No. It re-enables string-to-code compilation for any string, which is the behavior most XSS attacks abuse. Remove it and use AOT builds, plain functions, and 'wasm-unsafe-eval' for WebAssembly.

What is the difference between 'unsafe-eval' and 'wasm-unsafe-eval'?

'unsafe-eval' allows eval, new Function, and string timers as well as WebAssembly. 'wasm-unsafe-eval' allows only WebAssembly compilation and leaves the rest blocked, so it is the narrower, safer choice when WASM is all you need.

Does Angular need 'unsafe-eval'?

Only in just-in-time mode. The default production AOT build compiles templates at build time, so it does not need 'unsafe-eval'. Confirm you are not shipping a JIT build to production.

Sources