Fixing CSP Errors That Block Wasm Compilation
This page answers one task: the console shows CompileError: WebAssembly.instantiate(): Refused to compile or instantiate WebAssembly module because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive: "script-src 'self'" (or a similar message), and you
need WebAssembly to work without weakening the page’s Content Security Policy more than necessary.
Prerequisites
- [ ] Access to where the policy is set: an HTTP header, a
<meta http-equiv>tag, a framework config or an extension manifest. - [ ] The current policy text, from the response headers or the meta tag.
- [ ] The list of browsers you support.
Why CSP blocks WebAssembly at all
A Content Security Policy restricts where a page can load and execute code from. Its script-src directive governs JavaScript — and, by specification,
the compilation of WebAssembly, which is code generation from bytes much like eval is code generation from strings. Originally the only way to allow it
was 'unsafe-eval', which also allows eval(), new Function() and string arguments to setTimeout — exactly the JavaScript features strict policies
exist to forbid. That trade-off kept many security-conscious sites away from WebAssembly.
The 'wasm-unsafe-eval' source keyword solves this. It allows WebAssembly compilation and instantiation — WebAssembly.compile, instantiate,
instantiateStreaming, new WebAssembly.Module — and nothing else: JavaScript eval remains blocked. All current browsers support it. So the fix for
nearly every case is to add 'wasm-unsafe-eval' to script-src and leave 'unsafe-eval' out.
Step 1 — find the effective policy
A page can have several policies at once — one from a header, another from a meta tag, a third from a reverse proxy — and the browser enforces all of them; any one can block compilation. Collect them:
curl -sI https://app.example.com/ | grep -i content-security-policy
curl -s https://app.example.com/ | grep -io '<meta[^>]*content-security-policy[^>]*>'
In DevTools, the console error quotes the exact directive that blocked compilation, which tells you which policy to edit. Note that default-src is
used when script-src is absent, so a policy with only default-src 'self' blocks WebAssembly too.
Step 2 — add wasm-unsafe-eval to script-src
Content-Security-Policy: default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; connect-src 'self'; object-src 'none'; base-uri 'none'
If the policy uses nonces or hashes with 'strict-dynamic', add the keyword alongside them:
script-src 'nonce-r4nd0m' 'strict-dynamic' 'wasm-unsafe-eval'. The module file itself is fetched, not executed as a script, so .wasm URLs are governed
by connect-src when loaded with fetch — include the CDN origin there if modules come from elsewhere. Frameworks that generate CSP headers (Next.js,
SvelteKit, Helmet for Express) accept extra script sources in their configuration; set it there rather than patching headers downstream.
Step 3 — cover workers
Dedicated workers created from script URLs get their own policy from the worker script’s response headers in some configurations, and inherit the
creating document’s policy for blob: and data: workers. If the module is compiled inside a worker and the error appears in the worker’s console, set
the same script-src with 'wasm-unsafe-eval' on the worker script’s response, or compile the module on the main thread and post the
WebAssembly.Module to the worker. Worker loading patterns are discussed in
loading Wasm in a Web Worker with ESM.
Step 4 — handle extensions and special hosts
Browser extensions follow the same rule with stricter defaults. Manifest V3 extensions cannot use 'unsafe-eval' at all in extension pages, but may add
'wasm-unsafe-eval' to content_security_policy.extension_pages in the manifest:
{
"manifest_version": 3,
"content_security_policy": {
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'"
}
}
Content scripts run under the host page’s policy in some browsers, which you do not control; compile modules in the extension’s service worker or an offscreen document instead, as covered in using Wasm in a Manifest V3 browser extension. Electron apps that set a CSP in their HTML need the same keyword.
Step 5 — verify and monitor with report-only
Before tightening an existing policy, deploy it in Content-Security-Policy-Report-Only mode with a reporting endpoint, so violations are reported but
not enforced. Check that no WebAssembly-related violations appear, then switch to enforcement. In production, keep reporting enabled: a later change that
drops 'wasm-unsafe-eval' — for example a security team standardising headers — shows up in reports before users complain. Combined with the
recommendations in
writing a Content Security Policy for Wasm,
this keeps both the policy and the feature intact.
Why wasm-unsafe-eval is a reasonable risk
The keyword’s name sounds alarming, and security reviewers sometimes push back. It helps to explain what it permits. JavaScript eval is dangerous
because attackers who inject a string can turn it into code with the page’s full privileges. WebAssembly compilation can only produce code that runs in
the Wasm sandbox: it can touch its own linear memory and call only the imports the page explicitly provides. An attacker who could feed bytes to
WebAssembly.compile would still need the page to instantiate the result with useful imports and call it, and the page’s code decides that. The realistic
risk is therefore much smaller than 'unsafe-eval', which is why the keyword was introduced as a separate permission. The remaining concern — loading
modules from untrusted origins — is handled by connect-src restrictions on where modules can be fetched, by subresource-integrity-style hash checks, as
in
verifying Wasm integrity before instantiation,
and by never compiling bytes derived from user input.
Legacy browsers and older guidance
Older articles and Stack Overflow answers often recommend 'unsafe-eval' because 'wasm-unsafe-eval' did not exist or was not yet supported everywhere
when they were written. Support arrived in Chromium 97, Firefox 102 and Safari 16; if you must support older Safari, the choices are to accept
'unsafe-eval' for those users only (by varying the header per user agent, which is fragile), to use a JavaScript fallback such as wasm2js output for
them, or to drop support. For most audiences today the keyword alone is sufficient. Remove 'unsafe-eval' from policies that added it only for
WebAssembly; leaving it in silently re-enables eval for every script on the page.
Testing the policy in CI
Header changes are made by many hands — platform teams, CDN configuration, framework upgrades — so a regression test is worth having. Run an end-to-end
test that loads a page using WebAssembly with the production headers (served by the same configuration that deploys them, or fetched from a staging
deployment) and asserts that the module instantiates and that no securitypolicyviolation events fire. A short script can also parse the production
policy from curl -I and assert that script-src contains 'wasm-unsafe-eval' and does not contain 'unsafe-eval'. The second check protects the
strictness of the policy as much as the first protects the feature. Add both to the deployment smoke tests, so a change that silently disables WebAssembly
or weakens the policy is caught before users see it.
Expected output
With script-src 'self' 'wasm-unsafe-eval', the module compiles and runs; eval("1+1") in the console still fails with a CSP error; and report-only
monitoring shows no WebAssembly-related violations.
Gotchas
- Adding
'unsafe-eval'instead. It works but re-enables JavaScripteval. Use'wasm-unsafe-eval'. - Two policies, one forgotten. A meta tag or proxy header may still block it. Check every policy.
- Only
default-srcpresent. It applies to scripts too. Add an explicitscript-src. - Worker scripts with their own policy. Add the keyword there or compile on the main thread.
- Missing
connect-srcfor CDN-hosted modules. The fetch fails before compilation. Allow the origin. - Stale policies cached by a CDN. Header changes need a cache purge to take effect everywhere.
Performance note
CSP checks happen once per compilation call and added no measurable time in Chrome. The only performance effect of getting CSP wrong is indirect: sites
that fall back to wasm2js JavaScript to avoid the keyword ran the same workload 2–5× slower.
Frequently Asked Questions
Does 'wasm-unsafe-eval' allow eval()?
No. It applies only to WebAssembly compilation and instantiation.
Do I need it for instantiateStreaming only?
For every way of compiling: streaming, from bytes, and new WebAssembly.Module.
Can I restrict which modules may be compiled?
Not with CSP itself; restrict where they can be fetched with connect-src and verify hashes before compiling.
Is it needed in Node or Deno? No — CSP is a browser mechanism. Deno has its own permission model instead.
How do I listen for CSP violations in the page?
Add a securitypolicyviolation event listener on document; it fires with the blocked directive and source, which you can report.
Does the order of keywords in script-src matter?
No. Source expressions are a set; the browser allows anything any of them permits.
Related
- Writing a Content Security Policy for Wasm — a full policy.
- Running Wasm in a sandboxed iframe — isolation beyond CSP.
- Using wasm2js as a fallback — the slower alternative.
- Fixing incorrect response MIME type errors — another loading failure.
← Back to Troubleshooting Common Wasm Errors