WSTG - Latest
WebAssembly Reconnaissance
| ID |
|---|
| WSTG-WASM-01 |
Summary
WebAssembly (Wasm) is a binary instruction format delivered to the browser as a compiled module. Because it is not human-readable at first glance, it is often treated as opaque and therefore safer than JavaScript. In practice, just like traditional desktop binaries, Wasm modules can be located, fingerprinted, and analyzed.
Reconnaissance is the first step in WebAssembly security testing. This phase focuses on locating Wasm modules within the web application, identifying the toolchain that produced them, checking for exposed debugging artifacts such as source maps, DWARF (Debugging With Attributed Record Formats), and the name section, and mapping the infrastructure attack surface (URLs, API endpoints, hosts) that the modules reveal.
This phase only collects information about the target. It deliberately stops before decompiling the code, reading its logic, or running the module under a debugger. Successfully gathering this intelligence provides the foundational map required for the subsequent testing phases.
This chapter focuses on browser-delivered Wasm modules. Reconnaissance of Wasm running outside the browser (WASI runtimes, edge or serverless platforms) differs, because modules are usually not served to the client and must be obtained through other means. See the WebAssembly Testing Overview for the distinction.
Test Objectives
- Find all WebAssembly modules loaded by the target application, including those not named
.wasm, compressed, lazy-loaded, loaded throughfetch, and manual instantiation, or embedded in JavaScript. - Identify the compiler toolchain and source language (e.g., C/C++, Rust, Go, .NET) from section metadata, imports, exports, and glue code.
- Detect exposed debugging artifacts: source maps, DWARF information, and the
namecustom section. - Map the infrastructure attack surface by extracting embedded URLs, API endpoints, and internal hostnames or IP addresses.
How to Test
Locating WebAssembly Modules
WebAssembly binaries are loaded by JavaScript, usually asynchronously. The most direct way to discover them is through the browser’s developer tools and by inspecting the client-side JavaScript.
Browser Developer Tools
- Open Developer Tools and go to the Network tab.
- Reload the target application and interact with it so that lazily loaded features are triggered.
- In Chrome and Edge, select the Wasm filter to display only WebAssembly requests. Firefox has no dedicated Wasm filter, so search the request list for
.wasmand check the content type column forapplication/wasm. - Check Web Workers and Service Workers as well. They are common places where Wasm is loaded, and their traffic may appear under separate targets in the DevTools.
Inspecting the JavaScript Code
If the application uses lazy loading or bundling, search the JavaScript files for the WebAssembly loading APIs:
WebAssembly.instantiate/WebAssembly.instantiateStreamingWebAssembly.compile/WebAssembly.compileStreamingnew WebAssembly.Module/new WebAssembly.Instance
Applications do not always load Wasm through a plain .wasm URL. A module may be fetched with fetch or XMLHttpRequest and then instantiated manually from an ArrayBuffer, decompressed in JavaScript before instantiation, or assembled from several chunks. In these cases the Network tab shows only a generic request, so searching for the loading APIs above is the more reliable approach.
If a module is created at runtime and never appears as a downloadable request, it can be captured at the point of instantiation. For example, a script injected before the application code runs (using DevTools Local Overrides, a snippet run before page load, or a user-script extension) can wrap the API and keep a copy of the bytes:
const _instantiate = WebAssembly.instantiate;
window.__wasmDumps = [];
WebAssembly.instantiate = function (source, ...rest) {
if (source instanceof ArrayBuffer || ArrayBuffer.isView(source)) {
window.__wasmDumps.push(new Uint8Array(source.buffer || source).slice());
}
return _instantiate.call(this, source, ...rest);
};
The same wrapping approach applies to WebAssembly.compile, WebAssembly.Module, and the streaming variants (which receive a Response rather than bytes and require cloning it). The captured bytes can then be saved to a file for the analysis steps below.
Embedded Modules
Wasm binaries may also be embedded directly in JavaScript to avoid an extra HTTP request. Look for:
- Large Base64 strings passed through
atobor a custom decoder into aUint8Array. data:URIs such asdata:application/octet-stream;base64,.- Very long string literals, often containing many escaped or non-ASCII characters, that are decoded by a helper function at startup and passed to one of the APIs above.
Emscripten builds with the SINGLE_FILE option do this. Recent Emscripten versions embed the binary with a custom UTF-8 string encoding by default (Base64 is used only if SINGLE_FILE_BINARY_ENCODE is disabled), so the second and third patterns are the ones most likely to be seen in current builds. See the Emscripten settings reference for details. Because the encoding differs between versions, the instantiation-time capture shown above is often easier than decoding the string by hand.
Identifying Wasm Files by Content
Do not rely on the file extension or the MIME type alone. Modules may be served as .bin, .dat, or with no extension, or compressed as .wasm.gz or .wasm.br. Compressed files must be decompressed first (for example with gunzip or brotli -d) before the header can be checked. Browsers decompress Content-Encoding responses transparently, so a file saved from DevTools is usually already decompressed, while one fetched with a command-line tool may not be.
Every Wasm binary starts with the same 8-byte header (the magic bytes \0asm followed by the version):
00 61 73 6D 01 00 00 00
A downloaded file can be checked with file module.bin or od -An -tx1 -N8 module.bin. In JavaScript source, the same header appears as the Base64 prefix AGFzbQ.
If a file that the application loads as a module does not start with these bytes and is not compressed, the application may encode or encrypt it and decode it in JavaScript before loading it. In that case, read the decoding routine in the glue code and apply it to the downloaded file to obtain the real binary.
Once the header is confirmed, check that the module is valid using wasm-validate from the WebAssembly Binary Toolkit (WABT):
wasm-validate module.wasm
If validation fails because the module uses newer language features (for example garbage collection), retry with --enable-all.
Note that WebAssembly.instantiateStreaming and WebAssembly.compileStreaming require the server to send the application/wasm MIME type, so a Wasm file served with a different type may indicate a server misconfiguration.
Inspecting Custom Sections
Once a module is downloaded, list its sections with wasm-objdump (part of WABT). Most of the remaining reconnaissance steps are answered by this one output:
wasm-objdump -h module.wasm
Look for the following custom sections:
| Section | What it reveals | Covered in |
|---|---|---|
producers |
Source language and tools used to build the module | Identifying the Compiler and Toolchain |
sourceMappingURL |
URL of an external source map | Source Maps |
.debug_*, external_debug_info |
DWARF debug information, embedded or in a separate file | DWARF Debug Information |
name |
Original function and variable names | Name Section |
Custom sections are optional and can be removed at any time. A module with no custom sections at all is common in production builds.
Identifying the Compiler and Toolchain
Knowing whether a module was compiled from C/C++, Rust, Go, or another language helps the tester understand its memory model, calling conventions, runtime behavior, and expected JavaScript glue code.
This information can also help prioritize subsequent security testing. For example, a native-language toolchain may indicate that memory safety, linear-memory handling, unsafe operations, and native-to-JavaScript boundary interactions deserve closer examination, while framework-specific toolchains can point to their corresponding runtime and integration surfaces.
Compiler identification should be used to guide test selection and prioritization, not as evidence that a particular vulnerability is present.
Automated Detection
Tools such as WASM-Hunter can provide a quick compiler/toolchain identification by scanning the Wasm binary for characteristic strings, symbols, imports, and runtime markers.
wasm-hunter -compiler -i target_module.wasm
Automated identification is heuristic rather than definitive. Optimized, stripped, or obfuscated modules may remove or alter the markers used for detection, so the result should be treated as an initial classification and manually verified when possible.
Manual Identification
When automated detection is unavailable or produces an uncertain result, inspect multiple indicators rather than relying on a single signature.
The producers custom section is often a useful direct indicator because toolchains may record the languages and tools used to build the module, such as clang, rustc, or wasm-bindgen. You can inspect this using WABT’s wasm-objdump:
wasm-objdump -s -j producers module.wasm
The producers section is optional and is often missing from optimized or hardened builds. In that case, examine the module’s imports, exports, runtime strings, associated JavaScript glue code, and surrounding build artifacts. Imports and exports can be listed with wasm-objdump -x -j Import module.wasm and wasm-objdump -x -j Export module.wasm. Treat individual indicators as hints and, where possible, confirm the identification using a second independent indicator:
- Emscripten (C/C++): Imports such as
emscripten_resize_heap,__syscall_*, orinvoke_*. A genericenvimport module is not sufficient evidence because other toolchains use it as well. Release builds usually minify import names (for example, a module nameda), which hides these patterns. - WASI: An import module such as
wasi_snapshot_preview1orwasi_unstableindicates a build that uses WASI system calls. It does not identify the source language, and Emscripten modules import it too, so additional markers must be checked. - Rust (wasm-bindgen): Imports or exports starting with
__wbindgen_or__wbg_, an import module namedwbg, and a generated*_bg.wasmfile next to a*_bg.jsglue file. Theproducerssection typically listsrustcandwasm-bindgen. - Go: The
wasm_exec.jsglue file, an import module namedgojs(Go 1.21 and later) orgo(older releases), and strings such asruntime.wasmExit,runtime.gopanic, orGo build ID. Recent Go versions also add a custom section namedgo:buildid. - .NET (Blazor WebAssembly): A
dotnet.wasmordotnet.native.wasmfile loaded from a_framework/path. Before .NET 10, ablazor.boot.jsonmanifest is also present. From .NET 10 the boot configuration is inlined intodotnet.js. - AssemblyScript: Release builds have almost no markers: typically a single
env.abortimport and amemoryexport. Debug builds keep paths such as~lib/rt/in thenamesection, and modules built with--exportRuntimeexport functions like__newand__pin.
No single marker should be considered conclusive. The strongest identification comes from combining multiple independent indicators, such as producers metadata, runtime symbols, imports/exports, glue code, and deployment artifacts.
Stripping and obfuscation can defeat most of the markers above. Removing the producers and name sections, minifying import and export names, and tree-shaking runtime helpers are common in release builds, and a tester may be unable to identify the toolchain at all. An inconclusive result is a valid outcome of this phase and should be recorded as such, rather than guessed.
Testing for Source Maps and Debugging Symbols
Developers sometimes leave debugging artifacts in production builds. This is a finding by itself, and it also gives the tester a large amount of context.
Source Maps
Similar to JavaScript, a WebAssembly module can reference an external source map. The reference is stored in a custom section named sourceMappingURL, whose content is the URL of the map file (for example module.wasm.map).
If the section is not present, request likely locations such as module.wasm.map directly. The JavaScript glue code may also reference its own source map, through a //# sourceMappingURL= comment or a SourceMap response header. That map describes the JavaScript rather than the module, but it can still expose original source files.
If a map is accessible, open the Sources (or Debugger) tab in the browser. The browser parses the map and shows the original file layout. How much is recovered depends on the map:
- If the map contains
sourcesContent, or the referenced source files are downloadable, the original source code can be read. - Otherwise only file paths and line mappings are recovered, which still reveals project structure, dependency names, and developer paths.
DWARF Debug Information
For C, C++, and Rust, debugging data is more commonly shipped as DWARF than as a source map. At the reconnaissance stage, only check whether it is present: either as .debug_* sections inside the module, or as an external_debug_info section pointing to a separate debug file (“split DWARF”) that may be downloadable from the server.
Name Section
Even when source maps and DWARF are removed, the name custom section is sometimes left in place. It contains the original names of functions and, in some cases, local variables. Check for it in the section listing, and dump it with:
wasm-objdump -x -j name module.wasm
Names from Rust and C++ are usually mangled. Recovered names such as check_license_key or encrypt_password provide immediate targets for further analysis.
Attack Surface Mapping (URLs, APIs, and IPs)
WebAssembly modules often contain hardcoded infrastructure details. Typical examples include:
- Backend API base URLs and paths (
https://api.example.com/v2/,/internal/admin/). - WebSocket and other non-HTTP endpoints (
wss://realtime.example.com). - Staging, development, or internal hostnames (
build-server.corp.local,staging-api.example.com). - Private IP addresses and ports (
10.x.x.x,172.16-31.x.x,192.168.x.x). - Third-party service URLs, such as analytics, license-check, or update servers.
Extracting this routing information maps the application’s backend architecture and can reveal endpoints that are not referenced anywhere in the visible JavaScript.
Automated Extraction
wasm-hunter can automate this extraction from a local file or a remote URL:
wasm-hunter -i target_module.wasm
Manual Extraction
The same information can be collected with generic tools, which is useful when a dedicated tool is unavailable. String literals are stored in the module’s data segments, so they can be searched directly in the binary:
# URLs and endpoints
strings -n 6 module.wasm | grep -Eio '(https?|wss?|ftp)://[^[:space:]"'"'"']+'
# Common internal hostname patterns and API paths
strings -n 6 module.wasm | grep -Ei '\.(internal|local|corp|lan)\b|/api/|/v[0-9]+/'
# Private IPv4 addresses
strings -n 6 module.wasm | grep -Eo '\b(10\.[0-9]{1,3}|172\.(1[6-9]|2[0-9]|3[01])|192\.168)\.[0-9]{1,3}\.[0-9]{1,3}\b'
# Same search for UTF-16 strings (used by some toolchains such as .NET)
strings -n 6 -el module.wasm | grep -Ei 'https?://'
The raw data segments can also be dumped for manual review with wasm-objdump -s -j Data module.wasm. Strings that are compressed, encrypted, or assembled at runtime will not appear in these results, and strings found in the JavaScript glue code or in accompanying configuration files should be collected as well.
(Note: Extracting actual credentials, tokens, and cryptographic keys from the binary is outside the scope of reconnaissance.)
Related Test Cases
Remediation
- Do not ship source maps, DWARF data, or split debug files to production. Keep debugging and symbol information in a restricted symbol server or other controlled environment.
- Remove unnecessary debugging and build metadata from release builds. For example,
wasm-strip(WABT) removes all custom sections, andwasm-opt --strip-debug --strip-producers(Binaryen) removes debug information and theproducerssection. Configure the compiler and linker to produce stripped release artifacts where supported. - Do not hardcode internal hostnames, IP addresses, credentials, or sensitive endpoints in client-delivered modules. Treat anything delivered to the browser as public.
- Serve Wasm with the correct
application/wasmMIME type, and verify that source maps, debug artifacts, and other non-production build files are not publicly accessible.
Tools
- WebAssembly Binary Toolkit (WABT) - Includes
wasm-objdumpfor listing sections, imports, exports, and names, andwasm-strip. Also provideswasm2watandwasm-decompile, which belong to later analysis phases. - wasm-tools - Bytecode Alliance toolkit with a section and structure dump (
wasm-tools objdump) and a parser that can serve as an alternative to WABT. - Binaryen - Provides
wasm-opt, used for stripping debug information and theproducerssection, andwasm-disfor disassembly. - WASM-Hunter - Static analysis tool for Attack Surface Mapping and fast-path compiler detection, with direct remote URL scanning.
strings,od,file,grep- Generic utilities sufficient for header checks and basic string extraction.