Safeguard
Industry Analysis

Practical steps to secure third-party WebAssembly plugins...

A step-by-step guide to securing third-party WebAssembly plugins in production: sandboxing, capability restriction, resource limits, provenance checks, and runtime monitoring.

Aman Khan
AppSec Engineer
8 min read

Third-party WebAssembly plugins are quickly becoming the default extensibility model for API gateways, edge functions, CMS platforms, and data pipelines — and for good reason. Wasm modules are fast, portable, and sandboxed by design. But "sandboxed by design" is not the same as "secure by default." If you're wondering how to secure WebAssembly plugins before shipping a plugin marketplace or accepting community-contributed modules into production, the honest answer is that the Wasm sandbox is a strong starting point, not a finish line. Host integrations, over-permissioned WASI imports, and unverified plugin supply chains routinely undo the isolation guarantees people assume come for free.

This guide walks through a practical, sequential process for locking down untrusted Wasm modules in production: threat modeling the trust boundary, picking a runtime with real Wasm plugin sandboxing, applying Wasm capability restriction correctly, enforcing resource limits, verifying provenance, and monitoring at runtime. By the end, you'll have a concrete checklist you can apply to any plugin-hosting architecture, whether you're running wasmtime, wasmer, or a managed Wasm edge platform.

Step 1: Define the Trust Boundary Before You Load a Single Module

Before touching runtime flags, write down what a malicious or buggy plugin could actually do if it executed arbitrary logic inside your host process. Most teams skip this and jump straight to "Wasm is sandboxed, we're fine." That's the wrong mental model. The Wasm linear memory sandbox stops a plugin from directly touching host memory it wasn't given — it does not stop a plugin from:

  • Exfiltrating data through any host function you exposed to it
  • Reading files or environment variables via overly broad WASI mounts
  • Making outbound network calls if your host embeds a network-capable WASI implementation
  • Exhausting CPU, memory, or disk to cause denial of service
  • Calling back into host APIs with malformed input designed to trigger bugs in your glue code

Document the trust boundary explicitly: what data crosses from host to plugin, what crosses back, and what the plugin should never be able to reach (secrets, other tenants' data, the filesystem outside a scratch directory). This becomes your spec for every step that follows, and it's the artifact you'll hand to auditors when someone asks how untrusted Wasm modules are isolated in your architecture.

Step 2: Choose a Runtime That Implements Real Wasm Plugin Sandboxing

Not all embeddings are equal. wasmtime and wasmer both support fine-grained sandboxing, but only if you configure them that way. Start with a minimal, deny-by-default Store and Linker configuration rather than the "just works" defaults from a tutorial:

use wasmtime::*;

let mut config = Config::new();
config.consume_fuel(true);
config.epoch_interruption(true);
config.wasm_reference_types(false);
config.wasm_simd(false); // disable unless plugins require it

let engine = Engine::new(&config)?;
let mut linker = Linker::new(&engine);

// Do NOT call linker.func_wrap for anything you haven't reviewed.
// Only add the specific host functions the plugin contract requires.

Avoid wasmtime_wasi::add_to_linker with a default, fully-populated WasiCtx — that pattern hands plugins stdio, environment variables, and broad filesystem access out of the box. Build the WasiCtxBuilder explicitly and add capabilities one at a time so every grant is a conscious decision, not an inherited default.

Step 3: Apply Wasm Capability Restriction to Filesystem, Network, and Host Calls

This is where most "sandboxed" plugin systems quietly leak. WASI is capability-based, meaning a module can only access resources it was explicitly handed — but it's trivial to hand it more than intended.

Restrict filesystem access to a single scratch directory, never the real path:

let wasi = WasiCtxBuilder::new()
    .preopened_dir(scratch_dir_fd, "/plugin-data", DirPerms::all(), FilePerms::all())?
    .inherit_stdout() // consider dropping this too
    .build();

For network access, don't wire in a general-purpose sockets WASI proposal unless the plugin genuinely needs outbound calls. If it does, proxy those calls through a host function that enforces an allowlist of destinations, rather than giving the module raw socket capability:

linker.func_wrap("env", "http_fetch", |caller: Caller<'_, HostState>, url_ptr: i32, url_len: i32| -> i32 {
    let url = read_string(&caller, url_ptr, url_len)?;
    if !caller.data().allowed_hosts.contains(&host_of(&url)) {
        return Ok(ERR_DENIED);
    }
    // perform the fetch server-side, return a handle, never raw memory
});

The pattern to internalize: every host function you expose is part of your attack surface, and every parameter it accepts from the plugin needs the same input validation you'd apply to a public API endpoint. Wasm capability restriction only holds if the capabilities you grant are narrow and the host functions that mediate them are defensively written.

Step 4: Enforce Resource Limits — Fuel, Memory, and Deadlines

Isolation without resource limits just gets you a very well-contained denial-of-service. Set hard ceilings on every plugin invocation:

store.limiter(|state| &mut state.limits); // memory/table growth limits

store.set_fuel(10_000_000)?; // bounds total instructions executed

// In a separate thread, increment the epoch on a timer to enforce wall-clock timeouts
engine.increment_epoch();
store.set_epoch_deadline(1); // trap if the deadline passes before completion

Pair these with a StoreLimits configuration that caps linear memory growth and table size, and reject plugin binaries above a maximum module size at load time. If you're running plugins per-request in a multi-tenant service, also cap concurrent instances per tenant so one noisy plugin can't starve others sharing the host.

Step 5: Verify Plugin Provenance Before It Ever Reaches the Runtime

Sandboxing constrains what a plugin can do once it's running; it does nothing to tell you whether the .wasm binary you're about to load is the one your team actually reviewed. Treat every plugin artifact like any other software supply chain dependency:

  • Require signed plugin bundles and verify the signature before load, e.g. with cosign verify-blob --key plugin-publisher.pub plugin.wasm
  • Pin plugins by content hash (sha256) in your registry/manifest rather than by mutable version tags
  • Generate an SBOM for each plugin build (syft plugin.wasm -o cyclonedx-json) so you can track what's embedded inside it, including any statically linked C libraries compiled to Wasm
  • Scan plugin dependency trees for known CVEs before publishing them to your internal marketplace, the same way you'd scan a container image
  • Maintain an allowlist of approved plugin publishers, and require a review step for any new or updated plugin before it's promoted to production

This is the step teams skip most often because Wasm feels "safe already," but a perfectly sandboxed plugin built from a compromised or vulnerable dependency is still a liability — it just changes the blast radius rather than eliminating it.

Step 6: How to Secure WebAssembly Plugins at Runtime with Monitoring and Auditing

Static controls degrade over time as plugins update and host APIs evolve, so runtime visibility is the step that catches what steps 1–5 miss. At minimum:

  • Log every host function call a plugin makes, with arguments, so you can reconstruct behavior during an incident
  • Alert on fuel/epoch trap events — repeated timeouts from one plugin often indicate either a bug or an attempted resource-exhaustion attack
  • Track memory growth patterns per plugin instance; a plugin that suddenly grows its linear memory far beyond its historical baseline is worth investigating
  • Version-pin the runtime itself (wasmtime/wasmer) and subscribe to their security advisories — runtime CVEs affect every plugin you host, regardless of how well each plugin is scoped
  • Periodically re-run capability audits against your Step 1 trust boundary document; capability creep happens quietly as host functions get added for "just one feature"

Troubleshooting and Verification

Once controls are in place, verify them adversarially rather than assuming they hold:

  • Fuzz your host functions. Feed malformed, oversized, and boundary-value inputs to every function exposed across the host/plugin boundary. This is where most real-world Wasm sandbox escapes originate — not in the runtime itself, but in glue code.
  • Write a "hostile plugin" test suite. Build a small set of intentionally malicious Wasm modules — ones that try to read outside their preopened directory, spin infinite loops, allocate unbounded memory, or call host functions with unexpected argument types — and run them in CI against every runtime configuration change.
  • Confirm fuel and epoch limits actually trap. A common misconfiguration is enabling consume_fuel but never calling set_fuel, which silently disables the limit. Add an explicit test that asserts a runaway loop is terminated within your expected bound.
  • Diff capability grants on every deploy. If your WasiCtxBuilder configuration or host function allowlist changes, require a diff review — treat it with the same rigor as a firewall rule change.
  • Validate signature verification fails closed. Intentionally load an unsigned or tampered plugin binary in a staging environment and confirm the load is rejected, not just logged.

If a plugin misbehaves in production, capture the fuel/epoch trap logs, the host function call trace, and the plugin's content hash together — that combination is usually enough to determine whether you're looking at a bug, a misconfigured capability, or a genuinely malicious module that slipped past provenance checks.

How Safeguard Helps

Safeguard extends software supply chain security practices to Wasm plugin ecosystems the same way it does for container images and open-source packages. That means automated SBOM generation and dependency scanning for plugin binaries before they're published to an internal marketplace, provenance verification tied into your CI/CD pipeline so unsigned or unpinned modules never reach production runtimes, and continuous monitoring that flags anomalous host-function call patterns or capability drift against your approved baseline. For teams building plugin-based platforms on wasmtime or wasmer, Safeguard also helps codify capability policies as versioned, reviewable artifacts — so Wasm capability restriction isn't tribal knowledge in one engineer's runtime config, but an auditable control your whole organization can verify and enforce.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.