Compile Error (unsafe-eval)

Hello team,

I’m running into a CompileError related to our Content Security Policy (CSP). The error message is:

Refused to compile or instantiate WebAssembly module because 'unsafe-eval' is not an allowed source of script...

We are using PlayCanvas v1.66.3, and our CSP cannot include ‘unsafe-eval’ for security reasons.

Based on our investigation and similar reports online, we suspect this issue may be related to the Draco compression decoder. However, we’re not certain which part of the PlayCanvas loading pipeline is triggering it.

Could you please clarify:

  1. WebAssembly usage
    Which PlayCanvas components require WebAssembly compilation that browsers classify as unsafe-eval?
  2. CSP-friendly options
    Is there a recommended configuration or build that avoids the need for ‘unsafe-eval’ or can work with a stricter CSP?
  3. Fallback / detection
    If WebAssembly compilation is blocked, is there a supported way to detect this and, for example, load an uncompressed GLB or otherwise degrade gracefully?

Any guidance or best practices for running PlayCanvas with a strict CSP would be greatly appreciated.

Thank you,

Hello :slight_smile:

So regarding WebAssembly modules with unsafe eval is Basis.

Unfortunately is seems like it is using new Function in both the glue and fallback scripts so I am not confident it will work with a stricter CSP unless you explicitly allow unsafe-eval for both these files.

As for checking if WebAssembly compilation is blocked there is no PlayCanvas specific callback for this. We have a basisInitialize method but it does not currently throw an error - only silently fails. I would check support with this snippet taken from the engine code to check in advance:

    const wasmSupported = () => {
        try {
            if (typeof WebAssembly === 'object' && typeof WebAssembly.instantiate === 'function') {
                const module = new WebAssembly.Module(Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00));
                if (module instanceof WebAssembly.Module) {
                    return new WebAssembly.Instance(module) instanceof WebAssembly.Instance;
                }
            }
        } catch (e) {
            // handle here
    };

Hope this helps!

Kris

1 Like

Hi Kris,

Thank you very much!

These specific packages we exported don’t appear to be using Basis.

Is it possible that (if not the current) then a previous version of Draco included this eval? If that was the case, can we simply switch version (files) included in the packages?

Best,

[{"moduleName" : "DracoDecoderModule", "glueUrl" : "files/assets/184373158/1/draco.wasm.js", "wasmUrl" : "files/assets/184373159/1/draco.wasm.wasm", "fallbackUrl" : "files/assets/184373157/1/draco.js", "preload" : true}]

Here are the up to date versions of wasm files we use, I wonder if those are different to what you use?

Hello @mvaligursky and @KPal

After further testing, including upgrading to PlayCanvas 1.77 and removing/editing other scripts, we’ve confirmed the issue is with the Draco library.

This latest version included:

Do you have any suggestions on how we can resolve this?

Uncaught (in promise) CompileError: WebAssembly.compile(): 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

I’m sure you considered the option of not using Draco already. I’ll dump the suggestion from ChatGPT bellow, perhaps some other options are viable. We run draco in a worker, perhaps you could relax the rules there?

You’re hitting the browser’s CSP guardrail: by default, compiling/instantiating WebAssembly is treated like “eval”. If your script-src forbids it, calls like WebAssembly.compile/instantiate will be blocked with the error you posted. Modern CSP solves this with a dedicated switch: add 'wasm-unsafe-eval' to script-src (narrower than unsafe-eval, which also enables JS eval/new Function). MDN Web Docs+1

What in PlayCanvas uses WebAssembly (and is gated by CSP)

  • Draco mesh decoder (for KHR_draco_mesh_compression). Configure with dracoInitialize({ jsUrl, wasmUrl, ... }). api.playcanvas.com
  • Basis/KTX2 transcoder (for KHR_texture_basisu, .ktx2). Configure with basisInitialize({ wasmUrl, ... }). api.playcanvas.com
  • Ammo.js physics (if enabled). PlayCanvas ships the physics engine as a WASM build. developer.playcanvas.com
  • Meshoptimizer decoder (if using EXT_meshopt_compression): the popular decoder is WASM-based. npm+1
  • These (and other engine WASM helpers) are configured via pc.WasmModule.setConfig(...). Note there’s a fallbackUrl to a JS build, but see caveat below. api.playcanvas.com

CSP-friendly options (from strict → flexible)

  1. Keep main page strict; allow WASM only in a worker
  • Serve your Draco/Basis/physics worker script from a URL that responds with a relaxed CSP header, and load it as a Worker. Dedicated workers can have their own CSP (Chrome fixed this long ago), so the main document can stay locked down.
  • Main page CSP example (only allows workers from a safe host):
Content-Security-Policy:
  default-src 'self';
  script-src 'self';           /* no eval in the page */
  worker-src https://assets.example-cdn.com blob:;
  connect-src 'self' https://assets.example-cdn.com;
  • Worker response header (on https://assets.example-cdn.com/decoders/draco-worker.js):
Content-Security-Policy:
  default-src 'none';
  script-src 'self' 'wasm-unsafe-eval';
```Docs: worker CSP & `worker-src` directive; dedicated workers use the CSP delivered with the worker script response. [MDN Web Docs+1](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/worker-src?utm_source=chatgpt.com)
2. **Allow WASM in the page, but not JS eval**
  * Add **`'wasm-unsafe-eval'`** to `script-src` (do **not** add `unsafe-eval`). Example:

Content-Security-Policy:
default-src ‘self’;
script-src ‘self’ ‘wasm-unsafe-eval’;
worker-src ‘self’ blob:;


  * This enables `WebAssembly.compile/instantiate` without opening up general JS eval. [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/script-src?utm_source=chatgpt.com)
3. **Avoid runtime decoders entirely (no WASM needed)**
  * **Don’t use Draco or Meshopt codecs** at runtime. Instead, ship **plain glTF** optimized with **`KHR_mesh_quantization`** (no decoder required). `gltfpack` does this by default:

gltfpack -i in.glb -o out.glb


That preserves good size wins via quantization, while remaining CSP-friendly. [meshoptimizer.org+1](https://meshoptimizer.org/gltf/?utm_source=chatgpt.com)
  * For textures, avoid `.ktx2` (Basis) under strict CSP because it needs a WASM transcoder; use PNG/JPEG or pretranscoded GPU-native formats delivered conditionally per device. [github.khronos.org+1](https://github.khronos.org/KTX-Software/ktxjswrappers/msc_basis_transcoder.html?utm_source=chatgpt.com)

> ⚠️ **About JS fallbacks**
> Many asm.js/JS “fallbacks” (including Emscripten builds) use `new Function` under the hood and are *also* blocked when `unsafe-eval` is disallowed. So relying on `WasmModule.setConfig({ fallbackUrl: ... })` usually won’t help under a strict CSP. [api.playcanvas.com+1](https://api.playcanvas.com/engine/classes/WasmModule.html)

# Detection & graceful fallback

You can proactively test whether WASM compilation is allowed and choose assets accordingly:

async function canUseWasm() {
if (!(‘WebAssembly’ in window)) return false;
try {
// minimal empty module
const bytes = new Uint8Array([0,97,115,109,1,0,0,0]);
await WebAssembly.compile(bytes);
return true;
} catch (e) { return false; }
}

(async () => {
const wasmOk = await canUseWasm();

// Only initialize decoders when WASM is permitted
if (wasmOk) {
dracoInitialize({
jsUrl: ‘/vendors/draco/draco.wasm.js’,
wasmUrl:‘/vendors/draco/draco.wasm.wasm’,
numWorkers: 1
});
// basisInitialize(…) similarly if you use KTX2
}

// Choose a CSP-safe asset if WASM is blocked
const url = wasmOk ? ‘/models/thing-draco.glb’
: ‘/models/thing-quantized.glb’; // no Draco, uses KHR_mesh_quantization

app.assets.loadFromUrl(url, ‘container’, (err, asset) => {
if (err) {
// last-resort fallback to fully uncompressed
app.assets.loadFromUrl(‘/models/thing-uncompressed.glb’, ‘container’, () => {});
} else {
const entity = asset.resource.instantiateRenderEntity();
app.root.addChild(entity);
}
});
})();


PlayCanvas APIs referenced above: `dracoInitialize`, `basisInitialize`, and the `loadFromUrl` callback signature. [api.playcanvas.com+2api.playcanvas.com+2](https://api.playcanvas.com/engine/functions/dracoInitialize.html)

# Practical checklist

* If you control headers: prefer **`'wasm-unsafe-eval'`** (not `unsafe-eval`). [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/script-src?utm_source=chatgpt.com)
* If your org forbids even that:
  * Move decoders to a **Worker** served with its own CSP that allows `'wasm-unsafe-eval'`; ensure your page’s **`worker-src`** allows that origin (and `blob:` if you use Blob workers). [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/worker-src?utm_source=chatgpt.com)
  * Ship **quantized (non-Draco, non-Meshopt) glTF** and **non-KTX2 textures** (or pretranscoded GPU-native textures). [meshoptimizer.org+1](https://meshoptimizer.org/gltf/?utm_source=chatgpt.com)
* Expect Draco, Basis/KTX2, Ammo, and Meshopt decoders to require WASM. Wire them up with `dracoInitialize`/`basisInitialize`/`WasmModule` only when your preflight allows it. [api.playcanvas.com+2api.playcanvas.com+2](https://api.playcanvas.com/engine/functions/dracoInitialize.html)

If you share your current CSP header and which PlayCanvas features you’re using (Draco, Basis, Meshopt, Ammo), I can suggest the minimal, targeted change that keeps your policy tight while unblocking the pipeline.