Tracking CVEs Across the Wasm Runtime Supply Chain

Tracking CVEs Across the Wasm Runtime Supply Chain

Problem

The Wasm runtime ecosystem has matured rapidly: Wasmtime (Bytecode Alliance), WasmEdge (CNCF), wasmer, wazero, and the Wasm engines embedded in V8 (Node.js, Deno, Chrome), SpiderMonkey (Firefox), and JavaScriptCore (Safari) are all in active production use. Each has its own CVE history, its own release cadence, and its own advisory disclosure process.

Unlike application dependencies where vulnerability scanners (Trivy, Grype, Dependabot) can automatically detect vulnerable package versions, Wasm runtimes present a more complex tracking challenge:

Runtimes are embedded in unexpected places. Wasmtime is a Rust library that gets compiled into applications. WasmEdge is used as a containerd shim. Wazero is a Go library embedded in tools like opa (Open Policy Agent). An organisation may be running three different Wasm runtimes across their stack without realising it — because the runtime is a dependency of a dependency, not a direct package reference.

Runtime CVEs have wide blast radius. A vulnerability in a Wasm sandbox runtime is not equivalent to a CVE in an application library. If the runtime’s sandbox is broken (allowing guest code to escape), every Wasm module running on that runtime is affected. A CVE in Wasmtime’s sandbox could allow a malicious Wasm module (plugin, user-provided code) to escape containment — this is a qualitatively different risk from a library CVE.

CVE tracking infrastructure does not cover all runtimes equally. Wasmtime publishes security advisories to GHSA and via the Bytecode Alliance’s security advisory process. WasmEdge has had CVEs disclosed via GHSA. Wazero and wasmer are tracked in GHSA and OSV. But NVD enrichment for runtime CVEs can be slow, and scanner CPE matching for these components is inconsistent.

The supply chain depth makes automated detection hard. If your service uses opa and opa embeds wazero, your container image scanner will detect wazero as a transitive dependency — but only if the scanner handles Go module dependency trees, which varies by tool configuration.

Target systems: organisations embedding Wasm runtimes in production services; platform teams deploying WasmEdge as a containerd runtime; any stack using Wasmtime, wazero, or wasmer as a dependency; serverless platforms built on Wasm.


Threat Model

Adversary 1 — Sandbox escape via runtime CVE. A CVE in Wasmtime’s bounds checking logic allows a Wasm module to read or write outside its linear memory sandbox into the host process memory. An attacker who can supply a Wasm module (plugin system, user-provided functions) exploits the CVE to escape the sandbox and access host resources.

Adversary 2 — Denial of service via runtime resource exhaustion. A CVE in Wasmtime’s async execution (CVE-2024-class) allows a Wasm module to cause unbounded resource consumption in the runtime host. An attacker submits crafted Wasm that exhausts memory or CPU, causing DoS for all Wasm workloads on the affected host.

Adversary 3 — Undetected vulnerable runtime in transitive dependency. An organisation scans their container images but the scanner does not detect the wazero version embedded in OPA because Go module embedding is not fully analysed. A CVE is published for the embedded wazero version. The scanner shows clean. The organisation has no process to track Wasm runtime CVEs independently.

Adversary 4 — Malicious Wasm module exploiting patched-but-undeployed runtime. A sandbox escape CVE is published for Wasmtime. The vendor has released a patch. The organisation uses Wasmtime and is aware of the CVE. However, the Wasmtime library is embedded in three different services, each with different release cadences. One service is patched; two remain on the vulnerable version. An attacker exploits the unpatched services.


Configuration / Implementation

Step 1 — Inventory Wasm runtimes across your stack

#!/bin/bash
# scripts/wasm-runtime-inventory.sh
# Find Wasm runtimes deployed across the environment

echo "=== Wasm Runtime Inventory ==="

# 1. Check for standalone runtime binaries
echo ""
echo "--- Standalone Wasm runtimes ---"
for runtime in wasmtime wasmer wasm-pack wasm3 iwasm; do
    path=$(which "$runtime" 2>/dev/null)
    if [[ -n "$path" ]]; then
        version=$("$runtime" --version 2>/dev/null | head -1)
        echo "  $runtime: $path ($version)"
    fi
done

# 2. Check for WasmEdge as containerd shim
echo ""
echo "--- WasmEdge containerd shim ---"
if [[ -f /usr/local/bin/containerd-shim-wasmedge-v1 ]]; then
    VERSION=$(containerd-shim-wasmedge-v1 --version 2>/dev/null | head -1)
    echo "  WasmEdge shim: $VERSION"
fi

# 3. Check Kubernetes node for Wasm runtimeclass
echo ""
echo "--- Kubernetes Wasm RuntimeClasses ---"
kubectl get runtimeclass -o json 2>/dev/null | \
    jq -r '.items[] | select(.handler | test("wasm|wasmtime|wasmedge|spin")) |
    "\(.metadata.name): handler=\(.handler)"' || echo "  kubectl not available"

# 4. Check Go binaries for embedded wazero
echo ""
echo "--- Go binaries with embedded wazero ---"
find /usr/local/bin /usr/bin /opt -maxdepth 3 -executable -type f 2>/dev/null | \
while read -r binary; do
    if strings "$binary" 2>/dev/null | grep -q "wazero\|wasm.*runtime\|wasmtime"; then
        echo "  $binary (contains wasm strings)"
    fi
done

# 5. Check container images in use
echo ""
echo "--- Container images with wasm runtimes (from running pods) ---"
kubectl get pods -A -o json 2>/dev/null | \
    jq -r '.items[].spec.containers[].image' | sort -u | \
while read -r image; do
    if echo "$image" | grep -qi "wasm\|wasmtime\|wasmedge\|spin"; then
        echo "  $image"
    fi
done

# 6. Check Rust binaries for embedded wasmtime
echo ""
echo "--- Rust binaries potentially embedding Wasmtime ---"
find /usr/local/bin /opt -maxdepth 3 -executable -type f 2>/dev/null | \
while read -r binary; do
    if strings "$binary" 2>/dev/null | grep -q "wasmtime\|cranelift"; then
        wasmtime_ver=$(strings "$binary" | grep -oP 'wasmtime [0-9]+\.[0-9]+\.[0-9]+' | head -1)
        [[ -n "$wasmtime_ver" ]] && echo "  $binary ($wasmtime_ver)"
    fi
done

Step 2 — Subscribe to Wasm runtime security advisories

#!/bin/bash
# scripts/wasm-advisory-monitor.sh
# Monitor GitHub Security Advisories for Wasm runtime repositories

GITHUB_TOKEN="${GITHUB_TOKEN:?Set GITHUB_TOKEN}"
STATE_FILE="/var/lib/wasm-advisory-monitor/state.json"
mkdir -p "$(dirname "$STATE_FILE")"

# Wasm runtime GitHub repositories to monitor
WASM_REPOS=(
    "bytecodealliance/wasmtime"
    "WasmEdge/WasmEdge"
    "wasmerio/wasmer"
    "tetratelabs/wazero"
    "bytecodealliance/wasm-micro-runtime"  # WAMR
    "bytecodealliance/lucet"  # Deprecated but still in use
)

check_advisories() {
    local repo="$1"
    local owner="${repo%%/*}"
    local name="${repo##*/}"
    
    curl -s -X POST "https://api.github.com/graphql" \
        -H "Authorization: bearer $GITHUB_TOKEN" \
        -H "Content-Type: application/json" \
        -d "{
            \"query\": \"query {
                repository(owner: \\\"$owner\\\", name: \\\"$name\\\") {
                    securityAdvisories: vulnerabilityAlerts(first: 10, states: [OPEN]) {
                        nodes {
                            securityAdvisory {
                                ghsaId
                                summary
                                severity
                                publishedAt
                                identifiers { type value }
                                cvss { score }
                            }
                            vulnerableVersionRange
                            firstPatchedVersion { identifier }
                        }
                    }
                }
            }\"
        }" | jq --arg repo "$repo" '
        .data.repository.securityAdvisories.nodes[] |
        {
            repo: $repo,
            ghsa: .securityAdvisory.ghsaId,
            severity: .securityAdvisory.severity,
            cvss: .securityAdvisory.cvss.score,
            summary: .securityAdvisory.summary,
            cves: [.securityAdvisory.identifiers[] | select(.type=="CVE") | .value],
            vulnerable_range: .vulnerableVersionRange,
            fixed_in: .firstPatchedVersion.identifier
        }'
}

echo "=== Wasm Runtime Security Advisory Check ==="
for repo in "${WASM_REPOS[@]}"; do
    echo ""
    echo "--- $repo ---"
    advisories=$(check_advisories "$repo")
    if [[ -z "$advisories" ]] || [[ "$advisories" == "null" ]]; then
        echo "  No open security advisories"
    else
        echo "$advisories" | jq -r '"  [\(.severity)] \(.ghsa): \(.summary[0:80])\n  Fixed in: \(.fixed_in // "no fix")\n  CVEs: \(.cves | join(", "))"'
    fi
done

Step 3 — Pin runtime versions with digest verification

# Dockerfile — pin wasmtime version with SHA256 digest
# Prevents supply chain substitution and ensures reproducible builds

FROM debian:12-slim

# Pin wasmtime version — update this when a new security release is published
# Check: https://github.com/bytecodealliance/wasmtime/releases
ARG WASMTIME_VERSION="26.0.1"
ARG WASMTIME_SHA256="abc123def456..."  # Replace with actual SHA256 from release

RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates && \
    curl -fsSL "https://github.com/bytecodealliance/wasmtime/releases/download/v${WASMTIME_VERSION}/wasmtime-v${WASMTIME_VERSION}-x86_64-linux.tar.xz" \
        -o /tmp/wasmtime.tar.xz && \
    echo "${WASMTIME_SHA256}  /tmp/wasmtime.tar.xz" | sha256sum --check && \
    tar -xf /tmp/wasmtime.tar.xz -C /usr/local/bin/ --strip-components=1 \
        "wasmtime-v${WASMTIME_VERSION}-x86_64-linux/wasmtime" && \
    rm /tmp/wasmtime.tar.xz && \
    apt-get remove -y curl && apt-get autoremove -y && rm -rf /var/lib/apt/lists/*
# Cargo.toml — pin wasmtime crate version
[dependencies]
# Pin exact version — update when security releases are published
# Subscribe to: https://github.com/bytecodealliance/wasmtime/security/advisories
wasmtime = "=26.0.1"

# If using wasmtime features, be specific
[dependencies.wasmtime]
version = "=26.0.1"
default-features = false
features = ["cranelift", "component-model"]
# Do NOT use: async (if not needed) — reduces attack surface
// go.mod — pin wazero version
module github.com/example/myservice

go 1.22

require (
    // Pin exact version — update when CVEs are published
    // Advisory feed: https://github.com/tetratelabs/wazero/security/advisories
    github.com/tetratelabs/wazero v1.7.2
)

Step 4 — Renovate configuration for Wasm runtimes

{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": ["config:base"],
  
  "packageRules": [
    {
      "description": "Wasm runtime updates — require security review; no auto-merge",
      "matchPackageNames": [
        "wasmtime",
        "github.com/tetratelabs/wazero",
        "wasmer",
        "wasmedge"
      ],
      "matchDatasources": ["cargo", "go", "npm", "pypi"],
      "automerge": false,
      "labels": ["security", "wasm-runtime", "requires-review"],
      "reviewers": ["platform-team", "security-team"],
      "prPriority": 15,
      "prTitle": "chore(wasm): update {{depName}} {{currentVersion}} → {{newVersion}} (check CVEs)"
    },
    
    {
      "description": "WasmEdge container image updates",
      "matchPackageNames": ["wasmedge/wasmedge", "ghcr.io/wasmedge/wasmedge"],
      "matchDatasources": ["docker"],
      "automerge": false,
      "labels": ["security", "wasm-runtime"],
      "reviewers": ["platform-team"]
    }
  ],
  
  "vulnerabilityAlerts": {
    "enabled": true,
    "labels": ["security", "vulnerability"]
  }
}

Step 5 — CVE-triggered runtime update automation

#!/bin/bash
# scripts/wasm-runtime-cve-response.sh
# When a CVE is published for a Wasm runtime, trigger the update process

CVE_ID="${1:?Usage: $0 <cve-id> <runtime-name> <patched-version>}"
RUNTIME="${2:?}"
PATCHED_VERSION="${3:?}"

echo "=== Wasm Runtime CVE Response: $CVE_ID ==="
echo "Runtime: $RUNTIME"
echo "Patched version: $PATCHED_VERSION"

# 1. Find all services embedding this runtime
echo ""
echo "Finding services with embedded $RUNTIME..."

case "$RUNTIME" in
    wasmtime)
        # Search Cargo.lock files in repositories
        find . -name "Cargo.lock" | xargs grep -l "wasmtime" 2>/dev/null
        ;;
    wazero)
        find . -name "go.sum" | xargs grep -l "tetratelabs/wazero" 2>/dev/null
        ;;
    wasmedge)
        kubectl get pods -A -o json 2>/dev/null | \
            jq -r '.items[] | select(.spec.runtimeClassName == "wasmedge") |
            "\(.metadata.namespace)/\(.metadata.name)"'
        ;;
esac

# 2. Create emergency update PRs
echo ""
echo "Creating update PRs..."

case "$RUNTIME" in
    wazero)
        # Update go.mod in affected repositories
        find . -name "go.mod" -exec grep -l "tetratelabs/wazero" {} \; | \
        while read -r gomod; do
            repo_dir=$(dirname "$gomod")
            pushd "$repo_dir" > /dev/null
            go get "github.com/tetratelabs/wazero@${PATCHED_VERSION}"
            go mod tidy
            git diff go.mod go.sum
            # Create PR via gh cli
            gh pr create \
                --title "security: update wazero to $PATCHED_VERSION ($CVE_ID)" \
                --body "Emergency security update for $CVE_ID in wazero. Patched version: $PATCHED_VERSION" \
                --label "security,critical" 2>/dev/null || echo "PR creation requires gh auth"
            popd > /dev/null
        done
        ;;
    wasmtime)
        find . -name "Cargo.toml" -exec grep -l "wasmtime" {} \; | \
        while read -r cargo_toml; do
            sed -i "s/wasmtime = \"=[0-9.]*\"/wasmtime = \"=${PATCHED_VERSION}\"/" "$cargo_toml"
        done
        ;;
esac

# 3. Alert platform team
if [[ -n "${SLACK_WEBHOOK:-}" ]]; then
    curl -s -X POST "$SLACK_WEBHOOK" \
        -H "Content-Type: application/json" \
        -d "{\"text\": \":warning: Wasm Runtime CVE Alert: $CVE_ID\n*Runtime:* $RUNTIME\n*Patched version:* $PATCHED_VERSION\nEmergency update PRs created for affected services.\"}"
fi

echo ""
echo "Response complete. Review and merge update PRs."

Expected Behaviour

Scenario Without Wasm CVE tracking With Wasm CVE tracking
Wasmtime CVE published Not detected by container scanner GHSA monitor alerts; affected services identified; update PRs created
Wazero CVE in transitive Go dependency Go module scanner may catch it Explicit Renovate rule for wazero triggers; security review required
WasmEdge shim CVE on Kubernetes nodes Node version not in image scanner scope Node-level runtime inventory identifies version; kubelet update triggered
Runtime version pinned to old version in Dockerfile Stays on vulnerable version Renovate detects update; PR opened with CVE note in title
Multiple services use different runtime versions Inconsistent patching Inventory script identifies all versions; CVE response script updates all

Trade-offs

Aspect Benefit Cost Mitigation
Exact version pinning in Cargo.toml Full control; no surprise updates Manual update required for every security release Automate via Renovate; add CVE check to PR description
GHSA monitoring per repository Catches runtime-specific advisories Many GitHub API calls; rate limits Use GraphQL batching; cache results; run 4×/day not continuously
Inventory script for embedded runtimes Visibility into hidden runtime use strings on binaries is imprecise; may miss obfuscated embeds Combine with SBOM generation at build time for definitive inventory
Emergency update automation Fast response to runtime CVEs Auto-updating runtimes can introduce breaking changes Require test suite to pass before PR merges; keep a rollback plan

Failure Modes

Failure Symptom Detection Recovery
Wasm runtime embedded in compiled binary not detected by inventory script Runtime runs undetected; CVE tracking misses it Post-CVE audit finds untracked service Add SBOM generation to CI (cargo cyclonedx, go mod vendor analysis); check SBOM for wasm runtime packages
Renovate update breaks Wasm ABI compatibility Wasm modules fail to load after runtime update Wasm module load errors in application logs Pin both runtime version and Wasm binary format; test Wasm modules against new runtime in CI
GHSA rate limit prevents advisory checks Advisory monitor fails silently Monitor alert on failed API call Implement exponential backoff; use GitHub token with higher rate limits
WasmEdge shim update breaks containerd Containers fail to start after shim update Pod start failures; containerd logs show shim error Test shim update on one node before rolling out; maintain rollback node image