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:

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:

  1. Allocates memory in WASM linear memory via an exported malloc
  2. Writes bytes to a Uint8Array view of wasmInstance.exports.memory.buffer
  3. 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:

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
← Back to Blog