WebAssembly (WASM) Security Testing: Reversing, Analysis, and Vulnerabilities
WebAssembly was designed to run C, C++, and Rust code in the browser at near-native speed. It delivers on that promise — and it also brings the vulnerability classes those languages are famous for into a new context. Memory corruption, integer overflows, missing bounds checks: the same issues that plague native binaries can now exist inside a .wasm file running in someone's browser tab.
The security testing model for WASM-based applications is different from traditional web testing. You cannot simply read the source, intercept requests and modify parameters, or rely on error messages for feedback. You need to understand binary analysis, WebAssembly's linear memory model, and how the WASM runtime interacts with JavaScript and the DOM.
This guide covers how to analyse WASM binaries, find the vulnerability classes that appear most frequently, and test WASM-based applications as part of a web application security assessment.
WebAssembly Fundamentals for Security Testing
Before testing, understand the execution model. WebAssembly is a stack-based virtual machine that operates on a linear memory model — a contiguous byte array that the WASM module reads from and writes to directly. Unlike JavaScript, there is no garbage collector. Memory management is explicit, which means:
- Buffer overflows are possible when code writes past the end of an allocated region
- Use-after-free vulnerabilities exist when code accesses freed memory
- Integer overflows can corrupt memory layout
- Heap spray and heap grooming techniques apply
WebAssembly runs in a sandboxed environment — it cannot directly access the operating system, file system, or network. All external access goes through explicitly imported JavaScript functions. This limits the direct impact of exploitation but does not eliminate it: a WASM vulnerability can corrupt application data, bypass security checks, or cause denial of service entirely within the sandbox.
WASM files are binary-encoded but have a corresponding text format called WAT (WebAssembly Text Format). Security work mostly happens at the WAT level after decompiling the binary.
Reconnaissance: Finding WASM in a Target Application
Start by enumerating all WASM modules loaded by the application:
# In browser DevTools — Network tab, filter by Wasm
# Or from the console:
performance.getEntriesByType('resource').filter(r => r.initiatorType === 'fetch' || r.name.endsWith('.wasm'))
# From command line, spider the target and look for .wasm references:
wget -r -l 2 --spider https://target.com 2>&1 | grep -i "\.wasm"
# Inspect JavaScript for WebAssembly.instantiate calls:
grep -r "WebAssembly\." --include="*.js" .
Once you find WASM modules, download them for static analysis:
# Download via curl
curl -O https://target.com/static/app.wasm
# Or intercept via Burp Suite — WASM files appear as binary responses
# Filter: Response type: application/wasm
Static Analysis: Decompiling and Reading WASM
Conversion to WAT Text Format
The wasm2wat tool from the WebAssembly Binary Toolkit (WABT) converts binary WASM to human-readable WAT:
# Install WABT
apt-get install wabt # or: brew install wabt
# Convert binary to text format
wasm2wat app.wasm -o app.wat
# View the output
head -100 app.wat
WAT is verbose but readable. Look for imported functions (syscall-equivalents), exported functions (entry points), and memory operations (i32.load, i32.store, memory.grow).
Decompilation with Ghidra and wasm-decompile
For more complex modules, use a decompiler to get pseudo-C output:
# wasm-decompile from WABT — produces readable pseudo-code
wasm-decompile app.wasm -o app_decompiled.dcmp
# Ghidra with the WebAssembly plugin
# 1. Install Ghidra: https://ghidra-sre.org/
# 2. Install the WebAssembly plugin from the Ghidra plugin repo
# 3. Import app.wasm as a WebAssembly binary
# 4. Auto-analyse — Ghidra will identify functions and data structures
# Radare2 with wasm support
r2 -A app.wasm # -A runs analysis
# Then: pdf @ entry0 (disassemble entry point)
# Or: afl (list all functions)
What to Look for in Static Analysis
| Pattern | Security Relevance |
|---|---|
Imports: env.memory |
Memory is shared with JS — corrupting it affects both sides |
Imports: env.eval or env.Function |
WASM can trigger JS eval — escalation path if input reaches WASM |
memory.grow calls |
Dynamic memory allocation — look for integer overflows in size parameter |
| Unchecked array indexing | Missing bounds checks on array access patterns |
| Hardcoded values in data segment | Secrets, API keys, internal URLs embedded in binary |
| Cryptographic implementations | Custom crypto — check for side channels, weak algorithms |
Extracting Secrets from WASM Binaries
Developers frequently compile credentials, API keys, and internal endpoints directly into WASM modules, assuming binary encoding provides obscurity. It does not.
# Extract all strings from a WASM binary
strings app.wasm | grep -E "(key|secret|token|password|api|http|bearer)"
# Look at the data segment in WAT output
grep -A 5 "(data" app.wat
# wasm-objdump from WABT — shows sections including data
wasm-objdump -d app.wasm # disassemble
wasm-objdump -x app.wasm # show all sections with details
Common finds: hardcoded API keys for third-party services, internal API base URLs, secret keys used for client-side "encryption" of local data, debug endpoints or admin paths not linked from the UI.
Client-Side Control Bypass via WASM Manipulation
Some applications implement security-critical logic in WASM assuming it is tamper-resistant. Common examples: license checks, premium feature gates, anti-cheat in browser games, DRM validation. These can often be bypassed.
Patching WASM at Runtime with DevTools
The browser's WebAssembly debugger allows breakpoint-based analysis. You can intercept execution and modify memory directly:
// In browser DevTools console — access WASM memory directly
// After page loads, find the WASM instance:
const wasmInstance = /* locate from JS globals, e.g. window.__wasmInstance */;
// Read linear memory
const memory = new Uint8Array(wasmInstance.exports.memory.buffer);
console.log(memory.slice(0, 64));
// Write to memory — patch a license check result
// First identify the memory address via breakpoint analysis, then:
memory[licenseCheckOffset] = 1; // flip boolean at known offset
Binary Patching
For persistent modification, patch the WASM binary and reload it:
# Step 1: Convert to WAT
wasm2wat app.wasm -o app.wat
# Step 2: Find the check — look for conditional branches in the function
# WAT: (if (i32.eqz (local.get $license_valid)) (then (call $show_error)))
# Change: (if (i32.const 1) (then (call $show_error))) ; never fires
# Step 3: Convert back to binary
wat2wasm app_patched.wat -o app_patched.wasm
# Step 4: Intercept the WASM fetch with a proxy (e.g. Burp) and serve patched version
# Or use a browser extension to redirect .wasm URLs
Memory Corruption Vulnerabilities
WASM modules compiled from C/C++ can contain classic memory corruption bugs. While these do not give direct OS access (no syscalls), they can corrupt application state in ways that break security invariants.
Buffer Overflow
When WASM code processes attacker-controlled input — file uploads, form fields, URL parameters passed through JS — check for buffer overflows in the linear memory:
// Identify the JS bridge — how does JS pass data to WASM?
// Common pattern: data is written to linear memory, then WASM function is called
const ptr = wasmInstance.exports.malloc(inputLength);
const view = new Uint8Array(wasmInstance.exports.memory.buffer);
// JS writes input to ptr, then calls process(ptr, inputLength)
wasmInstance.exports.process(ptr, inputLength);
// Test: pass input longer than expected maximum
// Watch for: access violations, corrupted output, crashes
Integer Overflow in Allocation
If allocation size is computed from attacker input, integer overflow can cause undersized allocation followed by overflow:
// Vulnerable pattern in C compiled to WASM:
// void* buf = malloc(count * element_size);
// If count = 0x40000001 and element_size = 4:
// 0x40000001 * 4 = 0x100000004 → wraps to 4 on 32-bit
// malloc(4) allocates 4 bytes, but code then writes count * element_size bytes
// Test: send very large count values
// WASM operates on 32-bit integers by default — max is 4294967295
// Values near 2^31, 2^32 boundaries are interesting
Testing the JS-WASM Interface
The interface between JavaScript and WebAssembly is a critical attack surface. All data crossing this boundary should be scrutinized.
Exported Function Enumeration
// Enumerate all exported WASM functions
Object.keys(wasmInstance.exports)
// Try calling internal functions directly
// Some modules export helper functions not intended for direct use
wasmInstance.exports.internal_decrypt(ptr, length)
wasmInstance.exports.validate_token(tokenPtr)
wasmInstance.exports.check_admin(userId)
Input Handling Between JS and WASM
When JavaScript writes attacker data to WASM memory, it typically:
- Allocates memory in WASM linear memory via an exported
malloc - Writes bytes to a
Uint8Arrayview ofwasmInstance.exports.memory.buffer - Calls a WASM processing function with a pointer and length
Bypass opportunities exist when the JS side trusts length parameters from WASM, when WASM trusts pointer values from JS, or when the length passed to WASM differs from the actual allocated size.
Side-Channel Attacks on WASM
WebAssembly operations execute with more predictable timing than JavaScript. This makes timing side-channel attacks more reliable. If a WASM module implements cryptographic comparisons or permission checks, test for timing leaks:
// Measure WASM execution time with high-resolution timer
// Note: SharedArrayBuffer must be enabled for high-res timing
async function timingTest(input) {
const start = performance.now();
wasmInstance.exports.verify_token(ptr, input.length);
const elapsed = performance.now() - start;
return elapsed;
}
// Run multiple trials — look for timing differences based on input prefix
// A naive string comparison exits early on mismatch → shorter time for wrong prefix
WASM in the Context of a Full Pentest
For most web application pentests, WASM analysis fits into the broader assessment rather than being a standalone exercise. Prioritise WASM analysis when:
- The application processes sensitive data client-side (crypto, DRM, licensing)
- Security controls are implemented in WASM (authentication tokens, feature gates)
- The WASM module handles file uploads or parses user-supplied data
- You notice obfuscated JavaScript that loads WASM — suggests the developer knows binary analysis matters
Always check the network request that fetches the WASM file. Is it authenticated? Can you fetch it unauthenticated and analyse it offline? Is it cached without a content-integrity check, allowing interception?
Tooling Summary
| Tool | Use Case |
|---|---|
| WABT (wasm2wat, wasm-objdump) | Binary to text conversion, section inspection |
| Ghidra + WASM plugin | Full decompilation to pseudo-C, function analysis |
| Radare2 | Disassembly, WASM-aware analysis |
| Browser DevTools | Runtime debugging, memory inspection, breakpoints |
| Burp Suite | Intercept/replace WASM fetch, modify WASM inputs via JS bridge |
| strings | Quick secret extraction from binary |
| wasmer / wasmtime | Run WASM outside the browser for isolated analysis |
Ironimo's scanner analyses the JavaScript-WASM interface exposed by your application, flags WASM modules that process user input without visible server-side validation, and tests for client-side controls that can be bypassed. It handles the automated surface-level checks so your team can focus on the deep binary analysis that requires human judgment.
Start free scan