Auditing Third-Party Wasm Binaries
This page answers one task: a dependency, an SDK or a vendor gives you a .wasm file to ship in your application — check what it can
do, where it came from and what it contains, before it runs on your users’ machines.
Prerequisites
- [ ] The binary itself, plus any JavaScript glue that loads it.
- [ ] WABT (
wasm-objdump,wasm2wat) andwasm-tools. - [ ] The vendor’s documentation of what the module is supposed to do, and its licence terms.
What a binary review can and cannot tell you
A compiled WebAssembly module is harder to review than JavaScript but easier than native code. Its structure is fully specified and cheap to inspect: every import and export is listed by name, every custom section is labelled, and the code can be disassembled into a readable text form. What a review cannot do quickly is prove the absence of subtle misbehaviour inside thousands of functions. The practical goal is therefore not a complete proof but a short list of questions whose answers bound what the module can possibly do.
The most important question is about imports, because, as discussed in restricting what a module can import, a module can affect the outside world only through them. A module whose imports are three math helpers cannot exfiltrate data no matter what its code does. A module that imports a generic JavaScript call function can do anything the page can. Everything else in the audit adds context to that answer.
Step 1 — list imports and classify them
wasm-objdump -x -j Import vendor.wasm | sed -n 's/.*<- //p' | sort -u
env.emscripten_resize_heap
env.fd_write
env.invoke_iii
env.__syscall_openat
env._emscripten_fetch
wasi_snapshot_preview1.random_get
Classify each into one of three groups: harmless computation helpers (memory growth, math), I/O that the module’s stated purpose
explains (a codec that needs random_get for dithering), and I/O that it does not (_emscripten_fetch in an image decoder,
__syscall_openat in a pure computation library). The third group is what to raise with the vendor. For Emscripten modules, also read
the JavaScript glue: it implements those imports and decides what, for instance, _emscripten_fetch is actually allowed to reach.
Step 2 — check exports and their signatures
wasm-tools print vendor.wasm | grep -E '^\s*\(export'
Exports tell you what the module offers. Expect the documented API plus a few runtime helpers — allocation functions, a memory export,
a stack initialiser. A large number of unexplained exports often indicates a debug or “export everything” build, which is larger than
necessary and exposes internals; ask for a release build. Signatures that take or return externref deserve attention: they can hold
references to JavaScript objects and pass them around.
Step 3 — inspect custom sections and embedded strings
Custom sections carry metadata engines ignore: names, debug information, producer information, source maps, and anything a vendor chooses to embed.
wasm-objdump -h vendor.wasm | grep Custom
wasm-objdump -x -j producers vendor.wasm
strings -n 10 vendor.wasm | grep -Ei 'https?://|\.(com|net|io)/|token|secret|api[_-]?key' | sort -u
Custom start=0x00a1b2c3 end=0x00b4d5e6 (size=0x0012d223) ".debug_info"
Custom start=0x00b4d5e8 end=0x00b4d70a (size=0x00000122) "producers"
https://telemetry.vendor-sdk.io/v2/collect
A telemetry URL in a module whose imports include a fetch function is worth a conversation, even if it is benign. Embedded credentials
are a red flag regardless. Debug sections in a shipped binary are not a risk but are wasted bytes; they can be stripped with
wasm-tools strip if the licence allows modification. The producers section reveals which compilers and versions built the module,
useful for checking against known toolchain vulnerabilities.
Step 4 — establish provenance
Know where the binary came from and how to get the same one again. Record its hash, the vendor’s version string, where you downloaded it, and whether the vendor publishes hashes or signatures for releases. If the vendor provides source, check whether the binary can be reproduced from it — the strongest assurance available, and practical when the vendor documents the toolchain, as discussed in producing reproducible Wasm binaries.
sha256sum vendor.wasm >> third_party/HASHES
echo "vendor-sdk 4.2.1 downloaded $(date -u +%F) from https://downloads.vendor.example/sdk/4.2.1/" >> third_party/PROVENANCE
Pin that hash in the loader with fetch integrity checks so the reviewed binary is the one that runs. Check the licence too: a module
compiled from GPL or other copyleft code carries obligations, and the producers section and embedded strings often reveal which
libraries were linked.
Step 5 — automate the checks for every update
Vendors ship updates, and an audit done once is stale at the next version. Turn the checks into a script that runs in CI whenever the binary changes, comparing the new import list with the reviewed one:
#!/usr/bin/env bash
# scripts/audit-wasm.sh third_party/vendor.wasm
set -euo pipefail
f=$1
wasm-objdump -x -j Import "$f" | sed -n 's/.*<- //p' | sort -u > /tmp/imports.now
if ! diff -u third_party/vendor.imports.reviewed /tmp/imports.now; then
echo "✗ import list changed — re-review before merging"; exit 1
fi
if strings -n 10 "$f" | grep -Eiq 'api[_-]?key|secret'; then echo "✗ possible credential"; exit 1; fi
size=$(wc -c < "$f"); echo "size: $size bytes"
echo "✓ audit checks passed"
A changed import list does not mean the update is malicious — new features add imports — but it means a human should look before it ships. The same script can enforce a size budget, which catches a vendor accidentally shipping a debug build.
Making the audit proportionate
Not every third-party module deserves the same depth of review, and treating them all identically either wastes time on trivial dependencies or skimps on risky ones. A useful way to scale the effort is by two questions: what can the module reach, and what data does it see?
A module with only computation imports that processes public data — a font shaper, a syntax highlighter, a chart layout engine — needs the automated checks and a size budget, and little else. Even a buggy or malicious version can only produce wrong output. A module with only computation imports that processes sensitive data — a document parser that sees user files, a cryptographic library — needs more: provenance, reproducibility where possible, and attention to whether its outputs could leak data through what they contain. And a module with I/O imports that sees sensitive data — an analytics SDK, a remote-configuration client, an AI inference library that fetches its own models — warrants the full review, a conversation with the vendor about each network destination, and usually containment in a worker or sandboxed frame regardless of the answers.
Writing this classification down next to each dependency, together with the reviewed import list and hash, turns the audit into a record that survives team changes. When the next engineer updates the dependency, the record tells them what was checked, what was accepted and why, and which changes would need a fresh look.
Expected output
On an unchanged module, the script passes quietly. On an update that added network access:
--- third_party/vendor.imports.reviewed
+++ /tmp/imports.now
@@ -2,4 +2,5 @@
env.fd_write
env.invoke_iii
+env._emscripten_fetch
wasi_snapshot_preview1.random_get
✗ import list changed — re-review before merging
Gotchas
- Auditing the module and not the glue. For Emscripten and other toolchains, the JavaScript glue implements the imports and may itself make network requests. Review both.
- Trusting function names. Names in the
namesection come from the vendor’s build and can be misleading or absent. Judge by imports and behaviour. - One-time review. Updates change binaries. Automate the comparison.
- Stripping a licensed binary. Some licences forbid modification. Check before running
wasm-tools striporwasm-opton a vendor binary.
Performance note
The audit script runs in under a second on a 10 MB module. Its side findings often pay for themselves: in one review, a vendor SDK shipped with 6.1 MB of DWARF debug sections inside an 8.4 MB module; the vendor’s release build without them was 2.3 MB, cutting the application’s largest download by more than two thirds.
Frequently Asked Questions
Can I decompile a module to read its logic?
wasm2wat and wasm-tools print produce readable WAT, and decompilers such as wasm-decompile produce C-like pseudocode. Reading a
large module this way is slow; use it to answer specific questions raised by the earlier checks.
Is a module from a reputable vendor safe? Reputation reduces risk but does not remove supply-chain risk — builds can be compromised upstream. The import check is cheap enough to do regardless.
What if the module needs broad imports? Then contain it: run it in a worker or a sandboxed iframe with narrowed wrappers, as in running Wasm in a sandboxed iframe.
Does the browser verify anything about modules? Browsers validate that a module is well-formed and type-safe. They do not judge intent or provenance.
Related
- Inspecting modules with wasm-tools — the tools used throughout.
- Reading the import and export sections — what the listings mean.
- Security implications of Wasm in enterprise apps — policy around third-party code.
- Verifying Wasm integrity before instantiation — making sure the audited binary is the one that runs.
← Back to Browser Sandbox & Security Boundaries