WASM Plugin Architecture Threat Modeling: Trust Boundaries, Host-API Exposure, and Supply Chain

WASM Plugin Architecture Threat Modeling: Trust Boundaries, Host-API Exposure, and Supply Chain

Problem

WASM is the lingua franca for plugin systems in 2026: Envoy plugins, NGINX filters, Postgres extensions, ClickHouse UDFs, agent tool implementations, OBS streaming filters, Kong middleware, Spin services, edge runtime customer code. Each is a different shape with the same shared problem: untrusted (or partially-trusted) code runs in a privileged host process.

Security architects approach each plugin system as if it were unique. The reality is that plugin systems share a dozen recurring decision points, and the same structural mistakes recur across implementations:

  • Host-API surface decided expediently. “We expose a function that returns the current request body” — convenient for plugin authors, broad authority for the plugin.
  • Trust tiers conflated. First-party plugins (vendor-shipped) and third-party plugins (customer-uploaded) run with the same authority.
  • No supply-chain verification. Plugins distribute via OCI, npm, or vendor portals; signature verification is optional.
  • No resource quotas. A misbehaving plugin consumes the host’s CPU / memory.
  • State persistence ill-defined. Plugins read / write the host’s filesystem, KV stores, or shared memory in undefined ways.
  • Audit gaps. Plugin actions don’t show up in the host’s normal audit log.

This article is a threat-modeling framework for plugin architectures. It walks the structural decisions you face, shows the threats that emerge from each, and provides patterns for the common shapes (sidecar plugins, in-process plugins, queryable plugins).

The output: a per-plugin-system threat model that documents the trust boundary, the host-API exposure, the resource caps, the audit story, and the supply-chain controls. Same structure as the broader threat-modeling-at-scale practice; specialized for the WASM plugin shape.

Target systems: any plugin architecture using WASM (Envoy, NGINX, Postgres pg_wasm, agent tool runtimes, custom platforms). The framework applies regardless of host language or runtime.

Threat Model

The recurring adversaries:

  • Adversary 1 — Malicious plugin author: uploads a plugin to the host system. Wants to escape the WASM sandbox or abuse the host API.
  • Adversary 2 — Compromised vendor / supply-chain: legitimate plugin pipeline is compromised; updates carry malicious code.
  • Adversary 3 — Plugin abuse via privileged input: a plugin processes input the user controls; the user crafts input to trick the plugin into doing the wrong thing.
  • Adversary 4 — Cross-plugin attack: plugin A and plugin B share host resources; A reads B’s data via a shared cache or coordination channel.
  • Adversary 5 — Resource exhaustion: a plugin consumes host CPU / memory until the host service degrades.
  • Access level: Adversary 1 has plugin upload capability. Adversary 2 has the plugin’s build pipeline. Adversaries 3-4 have only normal user-level access to the host system. Adversary 5 has any plugin upload path.
  • Objective: Read or modify host data, escape the sandbox, abuse host privileges, deny service to other plugins or users.
  • Blast radius: depends entirely on the plugin architecture’s trust boundaries. Done well: bounded to the plugin’s own work and explicitly-shared resources. Done badly: arbitrary host access.

Configuration

Decision 1: Trust Tier of Plugins

Plugins fall into trust tiers; the architecture must distinguish them.

Tier Examples Risk Recommended controls
First-party / built-in Vendor-shipped components Low (audited code) Standard sandbox; full host-API as needed
Verified third-party Plugins from approved vendors Medium Sandbox + signed images; restricted host-API
Customer-uploaded End users upload arbitrary WASM High Sandbox + multi-tenancy + minimal host-API + per-tenant quotas

Most architectures fail by treating all plugins as Tier 1 (“we audited them”) even when end-users can ship Tier 3 code. Be explicit; document tier per plugin source; enforce tier-specific controls.

# plugin-tier-policy.yaml
tiers:
  tier_1:
    sources: ["vendor-shipped", "internal-plugins-repo"]
    capabilities: [filesystem_read_assets, network_outbound_allowlist, prometheus_emit]
    resource_limits:
      memory: 512MB
      cpu_seconds_per_call: 5
      concurrent_calls: 100

  tier_2:
    sources: ["partners-allowlist"]
    capabilities: [filesystem_read_assets, network_outbound_allowlist]
    resource_limits:
      memory: 128MB
      cpu_seconds_per_call: 2
      concurrent_calls: 20

  tier_3:
    sources: ["customer-uploaded"]
    capabilities: [memory_only, log_emit]
    resource_limits:
      memory: 32MB
      cpu_seconds_per_call: 0.5
      concurrent_calls: 5

Per-tier capability set and resource cap. New plugin upload classifies by source.

Decision 2: Host-API Surface

The host-API is the surface area where untrusted code makes requests of the host. Every host-API call is a security decision.

For each potential host-API function, ask:

  • What does the plugin learn from this call? Sensitive data exposure surface.
  • What does the plugin influence by this call? Authority granted.
  • Is this scoped to the plugin’s own data? Cross-tenant boundary.
  • Is this rate-limitable? Resource-exhaustion surface.
  • Does this leave audit traces the plugin author cannot tamper with? Forensic surface.

Classify host-API functions:

host_api_classification:
  read_safe:
    - get_request_path        # plugin sees the request URL path
    - get_request_method      # plugin sees the HTTP method
  read_sensitive:
    - get_request_header      # plugin sees Authorization, Cookie
    - get_request_body        # plugin sees user-submitted data
  write_local:
    - set_response_header     # plugin modifies its own response
    - set_response_status     # plugin sets HTTP status
  write_global:
    - dispatch_http_call      # plugin makes outbound HTTP — high risk
    - shared_kv_set           # plugin writes to shared store
  audit_only:
    - log_emit                # plugin emits to host log
    - metric_increment        # plugin updates Prometheus counter

Restrict per-tier:

# host_api_authorize.py
def authorize_host_call(plugin_tier: str, function: str) -> bool:
    if plugin_tier == "tier_1":
        return True   # full access
    if plugin_tier == "tier_2":
        return function not in ["dispatch_http_call_arbitrary"]
    if plugin_tier == "tier_3":
        return function in {
            "get_request_path",
            "set_response_header",
            "set_response_status",
            "log_emit",
            "metric_increment",
        }
    return False

The host evaluates every host-API call against this matrix. A Tier 3 plugin that attempts dispatch_http_call is denied at the boundary.

Decision 3: Resource Quotas

Per-plugin resource caps prevent one plugin from affecting others. Multiple dimensions:

  • Memory: linear-memory size cap per WASM instance.
  • CPU time: wall-clock or fuel-based cap per call.
  • Call concurrency: how many simultaneous invocations of this plugin.
  • Host-API call rate: rate-limit specific host-API functions per plugin.
  • Storage: if plugin can write to disk / KV, per-plugin quota.
// quota_check.rs
struct PluginQuota {
    memory_max: usize,
    cpu_seconds_per_window: f64,
    concurrent_calls_max: usize,
    host_api_calls_per_minute: HashMap<String, u32>,
}

fn admit_plugin_call(plugin_id: &str, current_state: &PluginState) -> Result<CallGuard, QuotaError> {
    if current_state.concurrent_calls >= state.quota.concurrent_calls_max {
        return Err(QuotaError::ConcurrencyLimit);
    }
    if current_state.cpu_used >= state.quota.cpu_seconds_per_window {
        return Err(QuotaError::CpuLimit);
    }
    Ok(CallGuard { plugin_id: plugin_id.into() })
}

Tier-bound quotas: Tier 3 plugins get tighter caps than Tier 1.

Decision 4: Plugin-Plugin Isolation

If multiple plugins coexist in the host, they share the host’s CPU and memory. They may also share host-managed state — a KV cache, a database connection pool, a configuration object.

plugin_plugin_isolation:
  state:
    shared_kv: tier_specific   # tier_1 plugins share; tier_3 each has own KV namespace
    config_object: per_plugin   # config never shared
  resources:
    db_connection_pool: per_plugin   # plugin gets its own pool, not shared
    network_egress: per_plugin       # outbound network rate-limited per plugin
  audit:
    other_plugins_logs: never_visible   # plugin never sees another plugin's log entries

For Tier 3, default to per-plugin everything. Sharing is opt-in with explicit security review.

Decision 5: Supply Chain

Where do plugins come from? How are they verified?

supply_chain:
  tier_1:
    source: ghcr.io/myorg/internal-plugins/*
    signature: required (cosign keyless via GitHub Actions OIDC)
    sbom: required (CycloneDX)
    in_toto: required (SLSA L3+)
    verification_at: deploy_time

  tier_2:
    source: ghcr.io/partners-allowlist/*
    signature: required (cosign with vendor's public key)
    sbom: optional but logged
    verification_at: deploy_time

  tier_3:
    source: customer_upload
    signature: optional
    static_analysis: required (capability-surface audit, IoC scan, vulnerable-dep scan)
    sandbox_test: required (run in isolation, observe behavior, before promoting to active)
    verification_at: upload_time

Each tier has a defined supply-chain control. Customer-uploaded plugins pass static analysis and a sandbox-test phase before going live.

Decision 6: Audit and Observability

Every plugin invocation must be auditable.

plugin_invocations_total{plugin_id, tier, outcome}
plugin_host_api_calls_total{plugin_id, function, allowed}
plugin_cpu_seconds_total{plugin_id}
plugin_memory_pages{plugin_id}
plugin_quota_rejected_total{plugin_id, reason}
plugin_egress_bytes_total{plugin_id, target}
plugin_supply_chain_violation_total{plugin_id, type}

Per-tier dashboards. Tier 3 has the strictest alerting:

  • Any host-API denial (allowed=false) triggers alert.
  • Egress to unexpected target — alert.
  • CPU or memory consistently near cap — alert.

Decision 7: Update / Rollback Flow

Plugins update over time. The flow needs to preserve isolation:

[New plugin version uploaded]
  -> [Static analysis on new version]
  -> [Verify signature / SBOM / SLSA]
  -> [Stage to canary tier (1% of traffic)]
  -> [Observe canary metrics for 30 minutes]
  -> [If healthy: promote to active]
  -> [If unhealthy: rollback to prior version]

Rollback must be automatic and immediate. A plugin update that crashes the host or causes spike in errors should never require manual intervention.

Decision 8: Per-Plugin Threat Model Document

Capture the per-plugin decisions in a document:

# plugin-threat-models/payments-helper.yaml
plugin_id: payments-helper
tier: tier_2
source: ghcr.io/payments-vendor/helper
host_api_capabilities:
  - get_request_path
  - get_request_header  (limited: only X-Tenant-Id)
  - set_response_header
  - log_emit
quotas:
  memory: 64MB
  cpu_seconds_per_call: 1
  concurrent_calls: 10
trust_decisions:
  - capability: "get_request_header (X-Tenant-Id only)"
    rationale: "Plugin needs to identify which tenant's logic to apply"
    risk: low
  - capability: "set_response_header"
    rationale: "Plugin sets a single internal header"
    risk: low
review:
  reviewed_by: security-team
  reviewed_at: 2026-04-29
  next_review: 2027-04-29

Standardize across plugins; review on changes.

Decision 9: Failure-Mode Scenarios

For each plugin system, walk through the failure modes:

failure_modes:
  plugin_crashes_during_request:
    impact: One request fails
    mitigation: Plugin sandbox traps; host returns appropriate error to user; metric incremented

  plugin_hits_cpu_quota:
    impact: Plugin trapped; one request fails
    mitigation: Quota mechanism; user gets timeout

  plugin_attempts_disallowed_host_call:
    impact: Single call rejected
    mitigation: Capability denial; log + alert

  plugin_supply_chain_compromise:
    impact: Plugin runs malicious code
    mitigation: Signature verification at upload; supply-chain attestation chain
    recovery: Revoke plugin; investigate; plugin tier may need to be tightened

  cross_plugin_state_leak:
    impact: One plugin reads another's data
    mitigation: Per-plugin namespace in shared state; no cross-plugin reads
    detection: State-store audit logs

  audit_pipeline_failure:
    impact: Plugin actions not recorded
    mitigation: Reliable audit pipeline; reject plugin invocation if audit can't be persisted

Each failure mode has a documented mitigation and recovery path.

Expected Behaviour

Signal Without threat model With threat model
New plugin’s host-API access Whatever was convenient Tier-bound by policy
Cross-plugin state visibility Often unbounded Per-plugin namespace
Resource exhaustion impact One plugin can starve others Bounded by quota
Plugin update flow Manual; risk of regression Automated canary + rollback
Audit completeness Inconsistent Per-call audit at host-API boundary
Trust-tier confusion Common Explicit; documented

Trade-offs

Aspect Benefit Cost Mitigation
Trust-tier policy Right-sized controls Maintenance of tier definitions Codify; review quarterly.
Capability matrix Bounds host-API Plugin authors lose flexibility Document for plugin authors; provide first-class capability requests.
Per-plugin quotas Bounded resource use More state to track Tracking is per-plugin; usually small compared to plugin count.
Plugin-plugin isolation Strong tenant boundary Some natural sharing patterns broken Document sharing patterns explicitly; design APIs to make sharing intentional.
Supply-chain verification Tamper detection Build pipeline complexity Standard now (cosign + SLSA + SBOM); reuse infrastructure.
Audit at host-API level Forensic visibility Logging volume Sample for high-frequency calls; log every denial.
Per-plugin threat-model docs Reviewable security posture Maintenance Tied to plugin lifecycle; new plugin = new document.

Failure Modes

Failure Symptom Detection Recovery
Tier creep (Tier 3 promoted to Tier 1 without review) Customer plugins gain elevated access Audit reveals capability mismatch Block escalation in policy; require explicit security review for tier changes.
Capability allowed without review Plugin gets a host-API it shouldn’t Audit at deploy time CI check: capability set must match approved threat-model document.
Quota too narrow Legitimate plugin throttled Plugin author reports issues Profile representative use; raise quota appropriately.
Supply chain bypass Plugin uploaded without verification Audit log shows unsigned plugin Refuse load; redeploy after fixing pipeline.
Cross-plugin shared-state misuse One plugin accesses another’s data Periodic audit + tracing Migrate to per-plugin namespace; revoke shared access.
Audit pipeline outage causes silence Plugin invocations not recorded Metrics show invocation rate but no audit events Fail-closed: refuse plugin invocation if audit can’t be persisted.
Update flow rolls forward despite failure Bad plugin version live Error rates spike post-update Auto-rollback policy must trigger on metrics regression; verify mechanism.