Security Guide

MCP server WebAssembly GC proposal security — cross-module struct sharing, unsynchronized array access, GC finalization timing oracle, type import aliasing

The WebAssembly GC proposal (now at Stage 4, shipping in Chrome and Firefox) adds garbage-collected heap types to Wasm: structs with named typed fields and arrays with element types. These types are reference types and can be passed across module instance boundaries as opaque references. What the spec does not provide is any access control on struct field reads once another module holds a reference to the struct, any synchronization primitives on GC array access, any guarantee about finalization order that prevents it from being used as a timing oracle, or any protection against type import aliasing that allows a struct type re-imported by a different module to be treated as having the same layout. Each of these gaps is an exploitable primitive in a multi-module MCP tool environment.

Cross-module struct field sharing — data leakage across module boundaries

A WebAssembly GC struct is a reference type. When a module exports a struct instance (by passing it to a host import or returning it from an exported function), any other module that receives that reference can read any field that the struct type exposes. There is no access-control modifier equivalent to private on struct fields in the current Wasm GC type system. If a module exports a struct type and passes a struct instance to another module, the other module can read all fields.

;; Module A — defines a struct type and exports an instance
;; (WAT — WebAssembly Text Format)

(module
  ;; Struct type with two fields: a public result and a "private" auth token
  (type $SessionData (struct
    (field $result (mut i32))
    (field $auth_token (mut i64))   ;; intended to be internal — no access modifier exists
  ))

  ;; Export a factory function that creates a SessionData struct
  (func $create_session (export "create_session") (result (ref $SessionData))
    (struct.new $SessionData
      (i32.const 42)          ;; result
      (i64.const 0xDEADBEEF)  ;; auth_token — attacker wants this
    )
  )
)

;; Module B — receives the struct reference and reads auth_token
;; (Wasm GC spec: any module holding a (ref $SessionData) can read all fields)

(module
  ;; Import the struct type from Module A by type reference
  (type $SessionData (struct
    (field $result (mut i32))
    (field $auth_token (mut i64))
  ))

  ;; Import the host function that gives us a SessionData reference
  (import "moduleA" "get_session" (func $get_session (result (ref $SessionData))))

  ;; Read the auth_token field — no access control prevents this
  (func $steal_token (export "steal_token") (result i64)
    (struct.get $SessionData $auth_token
      (call $get_session)
    )
    ;; Returns the auth_token field value — 0xDEADBEEF
    ;; Module B has no special permissions — the spec provides no field-level access control
  )
)

Wasm GC has no field-level access control. Any module that receives a struct reference can read any field on that struct type. MCP tools that use Wasm GC structs to carry session state, credentials, or intermediate computation results should never pass struct references across module boundaries unless all fields are safe to expose to any recipient.

GC array access — no synchronization, race conditions across module instances

Wasm GC arrays are reference types that can be passed across module instances just like structs. Unlike WebAssembly.Memory (which has a shared flag and the explicit agreement to use Atomics for synchronized access), Wasm GC arrays have no synchronization primitives. Multiple Wasm module instances in the same agent (or in a shared Worker) can hold a reference to the same GC array and call array.get and array.set concurrently without any atomicity guarantee. The result is a classic read-modify-write race condition at the GC layer.

;; Wasm GC array race condition — two module instances sharing a mutable array reference

;; The array type: mutable i32 elements
(type $SharedBuffer (array (mut i32)))

;; Module A (producer): writes results to the shared array
(func $write_result (param $arr (ref $SharedBuffer)) (param $idx i32) (param $val i32)
  ;; No atomic write — just a plain array.set
  (array.set $SharedBuffer
    (local.get $arr)
    (local.get $idx)
    (local.get $val)
  )
  ;; If Module B reads the same index concurrently, it may observe:
  ;; (a) the old value, (b) the new value, or (c) a torn intermediate state
  ;; (c) is engine-dependent — current V8 Wasm GC may not produce torn i32 writes
  ;; but the spec does not rule out tearing on wider types (i64, f64, externref)
)

;; Module B (consumer): reads from the shared array without synchronization
(func $read_result (param $arr (ref $SharedBuffer)) (param $idx i32) (result i32)
  ;; This read races with Module A's write — no Wasm GC atomic primitive exists
  (array.get $SharedBuffer
    (local.get $arr)
    (local.get $idx)
  )
)

;; Attack pattern: Module B (attacker-controlled) rapidly reads and writes
;; shared array slots while Module A performs a security-critical check-then-act:
;;   1. Module A reads array[0] → validates it as a safe command
;;   2. Module B writes array[0] → replaces with malicious command
;;   3. Module A acts on array[0] → executes the malicious command
;; This is a TOCTOU race at the Wasm GC array level.

GC finalization order as a timing oracle

When a Wasm GC struct or array has no more live references, it becomes eligible for collection. The engine decides when to collect it — this is non-deterministic and implementation-specific. However, the pattern of when GC collections happen (measured by how long object allocations take to start failing or by measuring allocation rate drops) correlates with GC pressure in the same heap partition. In a browser where same-process tabs share a V8 heap isolate, or where Agent Workers share a heap, the GC finalization timing of one module's objects correlates with the allocation activity of co-located tabs.

// JavaScript host code — measuring Wasm GC finalization timing as a side channel

// The attacker's MCP tool allocates a large number of Wasm GC structs
// and measures how long before they are collected, using a FinalizationRegistry
// as a proxy (FinalizationRegistry can hold WeakRefs to Wasm externref values)

const registry = new FinalizationRegistry((heldValue) => {
  const collectionTime = performance.now();
  console.log(`GC collected struct at: ${collectionTime}`);
  // Pattern of collection times correlates with heap pressure from other tabs
  // High-frequency collection → other tabs are allocating heavily
  // (e.g., large data processing — reveals background tab activity)
});

// Create a Wasm module that allocates GC structs and returns externref handles
async function measureGCPressure(wasmInstance) {
  const structs = [];
  for (let i = 0; i < 1000; i++) {
    const structRef = wasmInstance.exports.alloc_struct();
    // Register for finalization notification
    const sentinel = { id: i };
    registry.register(structRef, sentinel, sentinel);
    structs.push(structRef);
  }
  // Drop references — structs become GC-eligible
  structs.length = 0;

  // Measure time until first finalization callback fires
  // Fast finalization → GC ran under pressure → other tabs are allocating
  // This is a cross-tab information leak without any browser permission
}

GC pressure as a behavioral fingerprint. The rate at which Wasm GC objects are collected reveals the heap pressure of co-located content. An MCP tool running in a browser tab can use allocation-and-release loops to probe whether the user has other open tabs processing large datasets, which partially reveals browsing behavior without any explicit permission.

Type import/export aliasing — type-confused cross-module field access

The Wasm GC type system uses structural equivalence for type imports: if module B imports a struct type that has the same field layout as the struct type defined in module A, the engine treats them as the same type and allows cross-module field access. An attacker-controlled module can declare a struct type with the same field layout as a victim module's struct but with different semantic intent, and then request a reference to the victim's struct by providing a compatible type import declaration. The engine performs no semantic validation — only structural field layout must match.

;; Type import aliasing attack — attacker module declares a compatible type
;; to access fields of a victim module's struct

;; Victim module defines a CredentialBundle struct:
(module $victim
  (type $CredentialBundle (struct
    (field $user_id i32)
    (field $session_key i64)
    (field $permission_level i32)
  ))

  (func $get_bundle (export "get_bundle") (result (ref $CredentialBundle))
    ;; Returns a CredentialBundle for the current user session
    (struct.new $CredentialBundle
      (i32.const 1001)          ;; user_id
      (i64.const 0xABCDEF0123)  ;; session_key — sensitive
      (i32.const 7)             ;; permission_level — admin flag
    )
  )
)

;; Attacker module — re-declares the same struct layout under a different name
;; The Wasm engine's type compatibility check is structural: same fields = same type
(module $attacker
  ;; Same field layout — structurally identical to $CredentialBundle
  ;; Aliased as $AttackerStruct — the name doesn't affect type compatibility
  (type $AttackerStruct (struct
    (field $a i32)     ;; maps to user_id
    (field $b i64)     ;; maps to session_key
    (field $c i32)     ;; maps to permission_level
  ))

  (import "victim" "get_bundle" (func $get_bundle (result (ref $AttackerStruct))))

  (func $extract_session_key (export "extract_session_key") (result i64)
    ;; Read field $b — which is the victim's session_key
    (struct.get $AttackerStruct $b
      (call $get_bundle)
    )
    ;; Returns 0xABCDEF0123 — the victim module's session key
    ;; Engine allows this because the struct types are structurally equivalent
  )
)
Risk Wasm GC mechanism Defense
Cross-module struct field leakage No field-level access control — any module with a struct reference reads all fields Never pass struct references containing sensitive fields across module boundaries; use opaque handle indirection
GC array race condition No synchronization primitives on array.get/set — concurrent access races Use separate arrays per module instance; pass data by copying primitives, not shared GC array references
GC finalization timing oracle Finalization pattern correlates with heap pressure from co-located content Isolate Wasm execution in cross-origin isolated contexts (COOP/COEP); limit allocation-heavy tool code to dedicated Workers
Type import aliasing Structural type equivalence allows attacker module to declare compatible types and access fields Treat Wasm GC struct types as opaque; wrap sensitive structs in host-side handles that verify caller identity before field access

SkillAudit findings for WebAssembly GC misuse

Critical Wasm GC struct containing session credentials or keys passed to untrusted module instance. The MCP tool's Wasm module exports a struct reference that includes authentication tokens, session keys, or permission levels to a co-loaded module that is not part of the trusted tool bundle. Any recipient module can read all fields. Grade impact: −30.
High Mutable GC array reference shared across Wasm module instances without synchronization. Two or more Wasm module instances hold references to the same mutable GC array and perform concurrent reads and writes without any ordering guarantee. TOCTOU race conditions are possible on security-critical checks. Grade impact: −22.
Medium FinalizationRegistry registered on Wasm GC externref values in a tight allocation loop. The tool allocates and drops large numbers of Wasm GC objects and monitors finalization callbacks. The timing pattern leaks information about heap pressure from co-located content. Grade impact: −12.
Medium Wasm GC struct type exported with structurally identical layout to a sensitive host type. A tool-defined Wasm struct type has the same field layout as a host application struct, enabling any module that imports the same structural type to read sensitive fields from host-provided instances. Grade impact: −10.

Audit your MCP server for WebAssembly GC risks

SkillAudit checks Wasm GC usage patterns: struct reference exports, shared array access, FinalizationRegistry allocation loops, and type aliasing risks. Paste a GitHub URL and get a graded report in 60 seconds.

Run a free audit →