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) and wasm-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.

The audit, from cheapest to deepest check An audit of a third-party module checks imports first, then exports, custom sections and embedded strings, then provenance, licence and size, and only reads disassembly where the earlier checks raise a question. imports what can it reach? exports what does it offer? custom sections + strings what is embedded? provenance + licence where did it come from? disassembly only where needed

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.

Audit findings and how to respond Typical findings from a third-party module audit, how serious each is, and the usual response. finding severity response only computation imports low accept I/O imports the purpose explains medium narrow in the import object I/O imports it does not explain high ask vendor or sandbox it embedded URLs or tokens high clarify before shipping debug sections, many exports low risk, waste request release build

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 name section 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 strip or wasm-opt on 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.

A vendor module before and after requesting a release build Size of a third-party SDK module as first delivered, with debug sections included, and as rebuilt by the vendor in release mode after the audit flagged it. module size (MB) delivered (with DWARF) 8.4 MB vendor release build 2.3 MB

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.

← Back to Browser Sandbox & Security Boundaries