WebAssembly adoption has accelerated steadily since its MVP in 2017 and is now embedded across developer tools, media processing pipelines, cryptography implementations, game engines, and server-side runtimes. The security properties that make WASM appealing — sandboxed execution, memory isolation, near-native performance — are real, but frequently misunderstood in ways that create exploitable gaps in production deployments.
This guide covers the WASM security model accurately, the attacks that work against it, and the developer controls that actually reduce risk.
What the WASM Sandbox Actually Guarantees
WASM modules execute in a sandbox that prevents direct access to the host environment. A module cannot read arbitrary process memory, make system calls, or access the DOM or JavaScript objects unless the host explicitly provides those capabilities through imported functions.
What the sandbox does guarantee:
- Memory isolation: each WASM module operates in its own linear memory address space, inaccessible to other modules without explicit sharing
- No direct system calls: all I/O goes through JavaScript host functions that the host chooses to import
- Structured control flow: WASM’s bytecode format prevents unstructured jumps; the runtime validates the module before execution
- Type-checked imports and exports: the host and module agree on function signatures; mismatches are caught at instantiation time
What the sandbox does not guarantee:
- Safety within the module’s own linear memory
- Protection against logic bugs in the imported host functions
- Integrity of the module binary before it is loaded
Understanding this distinction matters because most exploitable WASM vulnerabilities fall into one of the categories the sandbox does not cover.
Linear Memory and Buffer Overflow Within WASM
WASM modules compiled from C or C++ with Emscripten or similar toolchains bring memory unsafety from those languages into the WASM context. The sandbox prevents the module from escaping to the host, but within linear memory, C-style buffer overflows, use-after-free bugs, and format string vulnerabilities all work exactly as they do natively.
The consequence is that a WASM module compiled from a vulnerable C library is a vulnerable WASM module. The sandbox eliminates certain exploitation paths (no shellcode to syscall, no overwriting function pointers to arbitrary addresses in host space) but does not eliminate the vulnerability class.
Example: Stack Smashing in Emscripten-Compiled Code
// Vulnerable C function compiled to WASM
void process_input(char *input) {
char buffer[64];
strcpy(buffer, input); // Classic stack smash — works in WASM linear memory
// ...
}
In WASM, this overwrites adjacent memory in the linear memory space. An attacker who controls this input may be able to corrupt data structures within the WASM module’s memory, including function table indices used for indirect calls (WASM’s equivalent of vtable function pointer manipulation).
Mitigation: Compile WASM modules from memory-safe languages where possible (Rust, Go, AssemblyScript). For C/C++ modules, enable Emscripten’s stack protector and sanitisers in testing:
emcc vulnerable.c -o output.wasm \
-fsanitize=address,undefined \
-s STACK_OVERFLOW_CHECK=2 \
-s ASSERTIONS=1
For production, compile with -O2 or -O3 and include Emscripten’s safe heap checks during pre-production testing to identify issues before shipping.
Indirect Call Table Exploitation
WASM implements function pointer-like behaviour through a function table. When C or C++ code compiles function pointers, they become indices into this table. An attacker who can overwrite an integer within WASM linear memory (e.g., through a buffer overflow) may be able to control which function is called at an indirect call site.
This does not provide arbitrary code execution in the traditional sense — WASM’s bytecode validation ensures only valid, statically-typed WASM functions can be in the table. But it can be used to divert program control to a different legitimate function, which may have security-relevant effects depending on what functions are available.
The Emscripten-compiled function table contains every function whose address is taken in the original C source. Reducing the size of this table by limiting function pointer usage reduces the exploitation surface:
# Wasm-opt can eliminate unused table entries
wasm-opt -Oz --dce input.wasm -o output.wasm
Host Import Function Security
The most impactful security decisions for WASM are in the host (JavaScript) code that defines what the module can do. Every capability the WASM module has comes from an imported function. Import principle of least privilege:
// Overly permissive: gives the module direct DOM access
const imports = {
env: {
get_document: () => document,
set_innerHTML: (element, html) => { element.innerHTML = html; }, // XSS risk
eval_js: (code) => eval(code), // Never do this
}
};
// Better: provide only what the module needs
const imports = {
env: {
log_message: (ptr, len) => {
const text = readString(memory, ptr, len);
console.log("[wasm]", sanitise(text)); // Sanitise before output
},
get_timestamp: () => Date.now(),
// Nothing that touches the DOM or executes code
}
};
The most common WASM security mistake is importing functions that bridge from WASM into JavaScript in ways that allow the WASM module to execute arbitrary script or access sensitive objects. A compromised or malicious WASM module with a poorly designed import surface becomes an XSS vector with native performance.
Content-Security-Policy for WASM
Browsers require 'wasm-unsafe-eval' in the script-src directive to allow WASM compilation from binary:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'wasm-unsafe-eval';
object-src 'none';
'wasm-unsafe-eval' does not allow general JavaScript eval(). It is narrower than 'unsafe-eval' and covers only WebAssembly compilation. Prefer it over the broader 'unsafe-eval'.
Restrict WASM sources using a nonce or hash where possible:
<script nonce="RANDOM_NONCE">
WebAssembly.instantiateStreaming(fetch('/module.wasm'), imports)
.then(result => { /* ... */ });
</script>
Content-Security-Policy:
script-src 'nonce-RANDOM_NONCE' 'wasm-unsafe-eval';
This prevents an attacker who can inject arbitrary JavaScript (e.g., via XSS in a script tag without the correct nonce) from loading additional WASM modules.
Supply Chain Risks: WASM Module Integrity
WASM modules are binary artifacts distributed through package registries or CDNs. A compromised npm package or CDN edge cache that replaces a WASM binary can introduce malicious native-speed code that executes in your users’ browsers.
Enforce Subresource Integrity (SRI) for WASM loaded from CDNs:
<!-- SRI hash verification for WASM loaded from external source -->
<script>
fetch('https://cdn.example.com/module.wasm', {
integrity: 'sha384-abc123...', // Precomputed hash of the expected binary
})
.then(r => r.arrayBuffer())
.then(buf => WebAssembly.instantiate(buf, imports));
</script>
For WASM distributed via npm (e.g., crypto libraries, codec packages), pin the exact dependency version and verify hashes in your lockfile. Auditing WASM binaries is harder than auditing JavaScript source because the binary is not human-readable — automated binary analysis tools (Wabt, wasm-objdump) can help, but the practical control for most teams is source-to-binary reproducibility checks and hash pinning.
Server-Side WASM Runtime Security
WASM is increasingly used server-side via WASI (WebAssembly System Interface) runtimes like Wasmtime, WASMEdge, and Wasm Workers (Cloudflare). The security model differs from browser WASM: the runtime provides access to system resources (filesystem, network, clock) through capability grants rather than a fixed host object model.
Apply capability minimisation:
// Wasmtime: grant only the specific directory access the module needs
let engine = Engine::default();
let mut linker = Linker::new(&engine);
wasmtime_wasi::add_to_linker(&mut linker, |s| s)?;
let wasi = WasiCtxBuilder::new()
.inherit_stdio()
.preopened_dir(
Dir::open_ambient_dir("/data/uploads", ambient_authority())?,
"/uploads", // Module sees this as /uploads, not the real path
)
// Do not grant network access unless required
// Do not grant host directory access beyond what is needed
.build();
The principle is the same as container security: grant capabilities explicitly and minimally, never inherit all host capabilities by default.
Key Takeaways for Developers
The WASM sandbox is real and provides meaningful isolation, but it does not eliminate memory safety bugs, does not protect against poor import surface design, and does not secure binary artifacts in transit. The practical security priorities for teams shipping WASM:
- Prefer memory-safe source languages (Rust, AssemblyScript) over C/C++ where the performance trade-off is acceptable
- Design import surfaces with least privilege — every function you import is a capability you grant the module
- Never import functions that allow the WASM module to execute script or manipulate the DOM without sanitisation
- Set
'wasm-unsafe-eval'in CSP, not the broader'unsafe-eval' - Pin WASM binary hashes and verify SRI for externally hosted modules
- For server-side WASI runtimes, grant directory and network capabilities explicitly
The threat model for WASM is different from JavaScript — attacks that require code execution in the host environment are blocked, but attacks within the module’s own memory space and attacks via the import surface are not. Understanding that distinction determines where security effort should focus.