WebAssembly Security Sandbox: 5 Production Patterns from Isolation to Capability Model

边缘计算

WebAssembly Security Sandbox: 5 Production Patterns from Isolation to Capability Model

In 2026, WebAssembly has moved from the browser to core production scenarios like edge computing, Serverless, and plugin systems. But "security" is never something that Wasm's linear memory model can handle on its own. An unconfigured WASI module can directly read host environment variables; a plugin without memory limits can drag the host process into an OOM abyss; an unverified third-party Wasm component may harbor supply chain attack trojans. This article covers 5 production patterns — isolation, capability, configuration, architecture, and audit — to help you build a truly secure Wasm runtime.

Security Threat Container Escape Wasm Sandbox Escape
Attack surface Syscalls + Namespace Linear memory + WASI
Historical CVEs CVE-2024-21626 etc. Very few (theoretical)
Defense in depth Seccomp + AppArmor Capability model + Memory limits + Timeout
Least privilege Manual configuration WASI default deny
Audit granularity Process-level Function-level

Core Concepts

Concept Full Name Description
Linear Memory Wasm module memory model: contiguous byte array, modules cannot access out of bounds, the foundation of sandbox isolation
WASI WebAssembly System Interface Standard interface for Wasm to access the OS; based on capability model, default deny all system access
Capability Model Security model where permissions must be explicitly granted rather than revoked after being granted by default
Sandbox Isolated execution environment; Wasm modules run in a sandbox and cannot directly access host resources
WasmEdge Lightweight Wasm runtime supporting AOT compilation, WASI-NN, resource limits, and other security features
WASI-NN WASI Neural Network Standard interface for Wasm AI inference; controls model access through capability model
Supply Chain Attack Attack by injecting malicious code into third-party dependencies; also exists in Wasm scenarios

Problem Analysis: Why Wasm Security Can't Rely on "Natural Isolation" Alone

1. Linear Memory Isolation ≠ System-Level Isolation

Wasm's linear memory model does prevent modules from reading and writing host memory out of bounds, but this is only memory-level isolation. Through WASI, Wasm modules can access the filesystem, network, and environment variables — if WASI permissions are misconfigured, "natural isolation" is an empty promise.

2. WASI Default Permissions Are Too Permissive

Many runtimes allow WASI modules to access the current working directory, environment variables, and network by default. In production, a malicious Wasm module can read /etc/passwd through WASI, make network requests to exfiltrate data, or even perform side-channel attacks through poll_oneoff.

3. Side-Channel Attacks Are Real

Spectre-style attacks are also possible in Wasm: through high-precision timers (performance.now or WASI's clock_time_get), a malicious module can infer the host process's memory layout. Research in 2025 has already demonstrated the feasibility of Wasm side-channels.

4. Supply Chain Attacks Are Ubiquitous

Your Wasm module depends on 10 third-party crates, one of which has been maliciously tampered with — this has happened multiple times in the Rust ecosystem (e.g., the rustdecimal incident). Wasm module compiled binaries are harder to audit, making supply chain attacks more隐蔽.

5. Multi-Tenant Scenarios Lack Resource Isolation

In plugin systems or Serverless platforms, multiple Wasm modules share the same runtime process. Without resource quotas (memory limits, CPU time, invocation counts), a malicious or runaway module can exhaust all resources, affecting other tenants.


Step-by-Step Patterns

Pattern 1: Wasm Linear Memory Isolation — Memory Bounds and Escape Prevention

;; Linear memory declaration: 1 page initial (64KB), max 256 pages (16MB)
(module
  (memory (export "memory") 1 256)

  ;; Safe memory access: with bounds checking
  (func (export "safe_read") (param $offset i32) (param $len i32) (result i32)
    (local $end i32)
    ;; Calculate end position
    (local.set $end (i32.add (local.get $offset) (local.get $len)))
    ;; Bounds check: ensure not exceeding memory range
    (if (i32.gt_u (local.get $end) (i32.mul (memory.size) (i32.const 65536)))
      (then (return (i32.const 0)))  ;; Return error code
    )
    ;; Safe read
    (i32.load (local.get $offset))
  )

  ;; Safe memory write: with bounds checking
  (func (export "safe_write") (param $offset i32) (param $value i32) (result i32)
    (local $end i32)
    (local.set $end (i32.add (local.get $offset) (i32.const 4)))
    (if (i32.gt_u (local.get $end) (i32.mul (memory.size) (i32.const 65536)))
      (then (return (i32.const 0)))
    )
    (i32.store (local.get $offset) (local.get $value))
    (i32.const 1)  ;; Success
  )
)
use std::alloc::{alloc, dealloc, Layout};

pub struct SafeMemory {
    buffer: *mut u8,
    size: usize,
    used: usize,
}

impl SafeMemory {
    pub fn new(size: usize) -> Self {
        let layout = Layout::from_size_align(size, 16).expect("invalid layout");
        let buffer = unsafe { alloc(layout) };
        if buffer.is_null() {
            panic!("memory allocation failed");
        }
        SafeMemory { buffer, size, used: 0 }
    }

    pub fn write(&mut self, data: &[u8]) -> Result<usize, &'static str> {
        if self.used + data.len() > self.size {
            return Err("buffer overflow: exceeds memory bounds");
        }
        unsafe {
            std::ptr::copy_nonoverlapping(
                data.as_ptr(),
                self.buffer.add(self.used),
                data.len(),
            );
        }
        let offset = self.used;
        self.used += data.len();
        Ok(offset)
    }

    pub fn read(&self, offset: usize, len: usize) -> Result<Vec<u8>, &'static str> {
        if offset + len > self.used {
            return Err("out of bounds read: exceeds used memory");
        }
        let mut result = vec![0u8; len];
        unsafe {
            std::ptr::copy_nonoverlapping(
                self.buffer.add(offset),
                result.as_mut_ptr(),
                len,
            );
        }
        Ok(result)
    }

    pub fn available(&self) -> usize {
        self.size - self.used
    }
}

impl Drop for SafeMemory {
    fn drop(&mut self) {
        let layout = Layout::from_size_align(self.size, 16).expect("invalid layout");
        unsafe { dealloc(self.buffer, layout) };
    }
}

unsafe impl Send for SafeMemory {}
unsafe impl Sync for SafeMemory {}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_safe_memory_bounds() {
        let mut mem = SafeMemory::new(1024);
        assert!(mem.write(b"hello").is_ok());
        assert!(mem.read(0, 5).is_ok());
        assert_eq!(mem.read(0, 5).unwrap(), b"hello");
        assert!(mem.read(999, 10).is_err());
    }
}
# Compile WAT to Wasm
wat2wasm safe_memory.wat -o safe_memory.wasm

# Validate memory bounds
wasm-validate safe_memory.wasm

# Run with WasmEdge and limit memory
wasmedge --memory-page-limit 256 safe_memory.wasm

# Run with Wasmtime and limit memory
wasmtime run --max-wasm-stack 512000 --wasm-max-memory 16 safe_memory.wasm

Pattern 2: WASI Capability-Based Security — Filesystem, Network, and Environment Permissions

use std::collections::HashMap;

#[derive(Clone, Debug)]
pub enum WasiPermission {
    FilesystemRead { path: String },
    FilesystemWrite { path: String },
    NetworkOutbound { host: String, port: u16 },
    EnvironmentVar { key: String },
    ClockAccess,
}

#[derive(Clone, Debug)]
pub struct WasiCapabilitySet {
    permissions: Vec<WasiPermission>,
    denied_defaults: Vec<String>,
}

impl WasiCapabilitySet {
    pub fn deny_all() -> Self {
        WasiCapabilitySet {
            permissions: vec![],
            denied_defaults: vec![
                "fs.read".into(),
                "fs.write".into(),
                "net.outbound".into(),
                "env.all".into(),
                "clock.highres".into(),
            ],
        }
    }

    pub fn allow_fs_read(mut self, path: &str) -> Self {
        self.permissions.push(WasiPermission::FilesystemRead { path: path.to_string() });
        self.denied_defaults.retain(|d| d != "fs.read");
        self
    }

    pub fn allow_fs_write(mut self, path: &str) -> Self {
        self.permissions.push(WasiPermission::FilesystemWrite { path: path.to_string() });
        self.denied_defaults.retain(|d| d != "fs.write");
        self
    }

    pub fn allow_net_outbound(mut self, host: &str, port: u16) -> Self {
        self.permissions.push(WasiPermission::NetworkOutbound {
            host: host.to_string(),
            port,
        });
        self.denied_defaults.retain(|d| d != "net.outbound");
        self
    }

    pub fn allow_env(mut self, key: &str) -> Self {
        self.permissions.push(WasiPermission::EnvironmentVar { key: key.to_string() });
        self
    }

    pub fn check_fs_read(&self, path: &str) -> bool {
        self.permissions.iter().any(|p| match p {
            WasiPermission::FilesystemRead { path: allowed } => path.starts_with(allowed),
            _ => false,
        })
    }

    pub fn check_fs_write(&self, path: &str) -> bool {
        self.permissions.iter().any(|p| match p {
            WasiPermission::FilesystemWrite { path: allowed } => path.starts_with(allowed),
            _ => false,
        })
    }

    pub fn check_net(&self, host: &str, port: u16) -> bool {
        self.permissions.iter().any(|p| match p {
            WasiPermission::NetworkOutbound { host: allowed, port: allowed_port } => {
                host == allowed && port == *allowed_port
            }
            _ => false,
        })
    }

    pub fn check_env(&self, key: &str) -> bool {
        self.permissions.iter().any(|p| match p {
            WasiPermission::EnvironmentVar { key: allowed } => key == allowed,
            _ => false,
        })
    }
}

fn main() {
    let caps = WasiCapabilitySet::deny_all()
        .allow_fs_read("/app/data")
        .allow_fs_write("/app/output")
        .allow_net_outbound("api.example.com", 443)
        .allow_env("APP_MODE");

    println!("Read /app/data/config.json: {}", caps.check_fs_read("/app/data/config.json"));
    println!("Read /etc/passwd: {}", caps.check_fs_read("/etc/passwd"));
    println!("Write /app/output/result.json: {}", caps.check_fs_write("/app/output/result.json"));
    println!("Write /tmp/malware: {}", caps.check_fs_write("/tmp/malware"));
    println!("Access api.example.com:443: {}", caps.check_net("api.example.com", 443));
    println!("Access evil.com:443: {}", caps.check_net("evil.com", 443));
    println!("Read APP_MODE: {}", caps.check_env("APP_MODE"));
    println!("Read DATABASE_URL: {}", caps.check_env("DATABASE_URL"));
}
# WasmEdge: Run WASI module with minimal privileges
wasmedge \
  --dir /app/data:/app/data:readonly \
  --dir /app/output:/app/output:writeonly \
  --env APP_MODE=production \
  --allow-env APP_MODE \
  module.wasm

# Wasmtime: WASI preview2 permission configuration
wasmtime run \
  --wasi preview2 \
  --dir /app/data::/app/data:readonly \
  --dir /app/output::/app/output:writeonly \
  --env APP_MODE=production \
  module.wasm

# Wasmer: Restrict network and filesystem
wasmer run module.wasm \
  --dir /app/data \
  --net=deny \
  --env=deny
[package]
name = "wasi-capability-demo"
version = "0.1.0"
edition = "2021"

[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"

[profile.release]
opt-level = 3
lto = true
codegen-units = 1
strip = true

Pattern 3: WasmEdge Sandbox Configuration — Resource Limits, Timeout, Memory Caps

use serde::{Deserialize, Serialize};
use std::time::{Duration, Instant};

#[derive(Serialize, Deserialize, Clone)]
pub struct SandboxConfig {
    pub max_memory_pages: u32,
    pub max_memory_bytes: u64,
    pub execution_timeout_ms: u64,
    pub max_table_size: u32,
    pub max_gas: u64,
    pub allowed_hosts: Vec<String>,
    pub allowed_env_vars: Vec<String>,
    pub max_fs_read_bytes: u64,
    pub max_fs_write_bytes: u64,
    pub max_network_requests: u32,
}

impl SandboxConfig {
    pub fn strict() -> Self {
        SandboxConfig {
            max_memory_pages: 256,
            max_memory_bytes: 16 * 1024 * 1024,
            execution_timeout_ms: 5000,
            max_table_size: 1024,
            max_gas: 1_000_000,
            allowed_hosts: vec![],
            allowed_env_vars: vec![],
            max_fs_read_bytes: 10 * 1024 * 1024,
            max_fs_write_bytes: 5 * 1024 * 1024,
            max_network_requests: 0,
        }
    }

    pub fn moderate() -> Self {
        SandboxConfig {
            max_memory_pages: 512,
            max_memory_bytes: 32 * 1024 * 1024,
            execution_timeout_ms: 30000,
            max_table_size: 4096,
            max_gas: 10_000_000,
            allowed_hosts: vec!["api.example.com".into()],
            allowed_env_vars: vec!["APP_MODE".into(), "LOG_LEVEL".into()],
            max_fs_read_bytes: 50 * 1024 * 1024,
            max_fs_write_bytes: 20 * 1024 * 1024,
            max_network_requests: 100,
        }
    }

    pub fn permissive() -> Self {
        SandboxConfig {
            max_memory_pages: 2048,
            max_memory_bytes: 128 * 1024 * 1024,
            execution_timeout_ms: 300000,
            max_table_size: 16384,
            max_gas: 100_000_000,
            allowed_hosts: vec!["*".into()],
            allowed_env_vars: vec!["*".into()],
            max_fs_read_bytes: 500 * 1024 * 1024,
            max_fs_write_bytes: 200 * 1024 * 1024,
            max_network_requests: 10000,
        }
    }
}

#[derive(Serialize, Deserialize)]
pub struct ExecutionResult {
    pub success: bool,
    pub output: String,
    pub memory_used_bytes: u64,
    pub execution_time_ms: u64,
    pub gas_used: u64,
    pub violation: Option<String>,
}

pub struct SandboxExecutor {
    config: SandboxConfig,
}

impl SandboxExecutor {
    pub fn new(config: SandboxConfig) -> Self {
        SandboxExecutor { config }
    }

    pub fn execute(&self, module_path: &str, function: &str, input: &str) -> ExecutionResult {
        let start = Instant::now();

        let memory_limit = format!("--memory-page-limit {}", self.config.max_memory_pages);
        let timeout = format!("--time-limit {}ms", self.config.execution_timeout_ms);

        let mut dir_args: Vec<String> = vec![];
        let mut env_args: Vec<String> = vec![];

        for host in &self.config.allowed_hosts {
            dir_args.push("--allow-host".to_string());
            dir_args.push(host.clone());
        }

        for env_var in &self.config.allowed_env_vars {
            env_args.push("--env".to_string());
            env_args.push(format!("{}=", env_var));
        }

        let output = std::process::Command::new("wasmedge")
            .arg(&memory_limit)
            .arg(&timeout)
            .args(&dir_args)
            .args(&env_args)
            .arg(module_path)
            .arg(function)
            .output();

        let elapsed = start.elapsed();

        match output {
            Ok(result) => {
                let stdout = String::from_utf8_lossy(&result.stdout).to_string();
                let stderr = String::from_utf8_lossy(&result.stderr).to_string();

                if result.status.success() {
                    ExecutionResult {
                        success: true,
                        output: stdout,
                        memory_used_bytes: 0,
                        execution_time_ms: elapsed.as_millis() as u64,
                        gas_used: 0,
                        violation: None,
                    }
                } else {
                    let violation = if stderr.contains("out of memory") {
                        Some("memory_limit_exceeded".to_string())
                    } else if stderr.contains("time limit") {
                        Some("execution_timeout".to_string())
                    } else {
                        Some(format!("runtime_error: {}", stderr.trim()))
                    };

                    ExecutionResult {
                        success: false,
                        output: stdout,
                        memory_used_bytes: 0,
                        execution_time_ms: elapsed.as_millis() as u64,
                        gas_used: 0,
                        violation,
                    }
                }
            }
            Err(e) => ExecutionResult {
                success: false,
                output: String::new(),
                memory_used_bytes: 0,
                execution_time_ms: elapsed.as_millis() as u64,
                gas_used: 0,
                violation: Some(format!("spawn_failed: {}", e)),
            },
        }
    }
}

fn main() {
    let configs = vec![
        ("strict", SandboxConfig::strict()),
        ("moderate", SandboxConfig::moderate()),
        ("permissive", SandboxConfig::permissive()),
    ];

    for (name, config) in configs {
        println!("=== {} sandbox ===", name);
        println!("  max_memory: {}MB", config.max_memory_bytes / 1024 / 1024);
        println!("  timeout: {}ms", config.execution_timeout_ms);
        println!("  max_gas: {}", config.max_gas);
        println!("  allowed_hosts: {:?}", config.allowed_hosts);
        println!("  allowed_env: {:?}", config.allowed_env_vars);
    }
}
# WasmEdge strict sandbox: 16MB memory + 5s timeout
wasmedge \
  --memory-page-limit 256 \
  --time-limit 5000 \
  --allow-host api.example.com \
  --env APP_MODE=production \
  /app/plugin.wasm

# WasmEdge moderate sandbox: 32MB memory + 30s timeout
wasmedge \
  --memory-page-limit 512 \
  --time-limit 30000 \
  --allow-host api.example.com \
  --allow-host cdn.example.com \
  --env APP_MODE=production \
  --env LOG_LEVEL=info \
  /app/plugin.wasm

# WasmEdge permissive sandbox: 128MB memory + 5min timeout (trusted modules only)
wasmedge \
  --memory-page-limit 2048 \
  --time-limit 300000 \
  /app/trusted_module.wasm
apiVersion: v1
kind: ConfigMap
metadata:
  name: wasm-sandbox-profiles
data:
  strict.yaml: |
    maxMemoryPages: 256
    executionTimeoutMs: 5000
    allowedHosts: []
    allowedEnvVars: []
    maxFsReadBytes: 10485760
    maxFsWriteBytes: 5242880
    maxNetworkRequests: 0

  moderate.yaml: |
    maxMemoryPages: 512
    executionTimeoutMs: 30000
    allowedHosts:
      - api.example.com
    allowedEnvVars:
      - APP_MODE
      - LOG_LEVEL
    maxFsReadBytes: 52428800
    maxFsWriteBytes: 20971520
    maxNetworkRequests: 100

  permissive.yaml: |
    maxMemoryPages: 2048
    executionTimeoutMs: 300000
    allowedHosts:
      - "*"
    allowedEnvVars:
      - "*"
    maxFsReadBytes: 524288000
    maxFsWriteBytes: 209715200
    maxNetworkRequests: 10000

Pattern 4: Plugin Isolation Architecture — Multi-Tenant Resource Quotas

use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, AtomicU32, Ordering};
use std::sync::Arc;

#[derive(Serialize, Deserialize, Clone)]
pub struct TenantQuota {
    pub tenant_id: String,
    pub max_memory_mb: u64,
    pub max_cpu_time_ms: u64,
    pub max_invocations_per_min: u32,
    pub max_network_requests: u32,
    pub allowed_paths: Vec<String>,
    pub allowed_hosts: Vec<String>,
}

pub struct TenantResourceUsage {
    memory_used_bytes: AtomicU64,
    cpu_time_used_ms: AtomicU64,
    invocations_count: AtomicU32,
    network_requests: AtomicU32,
}

impl TenantResourceUsage {
    pub fn new() -> Self {
        TenantResourceUsage {
            memory_used_bytes: AtomicU64::new(0),
            cpu_time_used_ms: AtomicU64::new(0),
            invocations_count: AtomicU32::new(0),
            network_requests: AtomicU32::new(0),
        }
    }

    pub fn try_allocate_memory(&self, bytes: u64, limit_mb: u64) -> bool {
        let limit_bytes = limit_mb * 1024 * 1024;
        loop {
            let current = self.memory_used_bytes.load(Ordering::Relaxed);
            if current + bytes > limit_bytes {
                return false;
            }
            if self.memory_used_bytes.compare_exchange_weak(
                current,
                current + bytes,
                Ordering::SeqCst,
                Ordering::Relaxed,
            ).is_ok() {
                return true;
            }
        }
    }

    pub fn try_invoke(&self, limit_per_min: u32) -> bool {
        let current = self.invocations_count.load(Ordering::Relaxed);
        if current >= limit_per_min {
            return false;
        }
        self.invocations_count.fetch_add(1, Ordering::SeqCst);
        true
    }

    pub fn try_network_request(&self, limit: u32) -> bool {
        let current = self.network_requests.load(Ordering::Relaxed);
        if current >= limit {
            return false;
        }
        self.network_requests.fetch_add(1, Ordering::SeqCst);
        true
    }

    pub fn reset_minute_counters(&self) {
        self.invocations_count.store(0, Ordering::SeqCst);
        self.network_requests.store(0, Ordering::SeqCst);
    }
}

pub struct PluginIsolationManager {
    tenants: HashMap<String, (TenantQuota, Arc<TenantResourceUsage>)>,
}

impl PluginIsolationManager {
    pub fn new() -> Self {
        PluginIsolationManager {
            tenants: HashMap::new(),
        }
    }

    pub fn register_tenant(&mut self, quota: TenantQuota) {
        let usage = Arc::new(TenantResourceUsage::new());
        self.tenants.insert(quota.tenant_id.clone(), (quota, usage));
    }

    pub fn check_invocation(&self, tenant_id: &str) -> Result<(), String> {
        let (quota, usage) = self.tenants.get(tenant_id)
            .ok_or_else(|| format!("unknown tenant: {}", tenant_id))?;

        if !usage.try_invoke(quota.max_invocations_per_min) {
            return Err(format!(
                "tenant {} exceeded invocation limit: {}/min",
                tenant_id, quota.max_invocations_per_min
            ));
        }

        Ok(())
    }

    pub fn check_memory(&self, tenant_id: &str, bytes: u64) -> Result<(), String> {
        let (quota, usage) = self.tenants.get(tenant_id)
            .ok_or_else(|| format!("unknown tenant: {}", tenant_id))?;

        if !usage.try_allocate_memory(bytes, quota.max_memory_mb) {
            return Err(format!(
                "tenant {} exceeded memory limit: {}MB",
                tenant_id, quota.max_memory_mb
            ));
        }

        Ok(())
    }

    pub fn check_network(&self, tenant_id: &str, host: &str) -> Result<(), String> {
        let (quota, usage) = self.tenants.get(tenant_id)
            .ok_or_else(|| format!("unknown tenant: {}", tenant_id))?;

        if !quota.allowed_hosts.iter().any(|h| host == h || h == "*") {
            return Err(format!(
                "tenant {} not allowed to access host: {}",
                tenant_id, host
            ));
        }

        if !usage.try_network_request(quota.max_network_requests) {
            return Err(format!(
                "tenant {} exceeded network request limit: {}",
                tenant_id, quota.max_network_requests
            ));
        }

        Ok(())
    }

    pub fn check_fs(&self, tenant_id: &str, path: &str) -> Result<(), String> {
        let (quota, _) = self.tenants.get(tenant_id)
            .ok_or_else(|| format!("unknown tenant: {}", tenant_id))?;

        if !quota.allowed_paths.iter().any(|p| path.starts_with(p)) {
            return Err(format!(
                "tenant {} not allowed to access path: {}",
                tenant_id, path
            ));
        }

        Ok(())
    }
}

fn main() {
    let mut manager = PluginIsolationManager::new();

    manager.register_tenant(TenantQuota {
        tenant_id: "tenant-alpha".into(),
        max_memory_mb: 32,
        max_cpu_time_ms: 5000,
        max_invocations_per_min: 100,
        max_network_requests: 50,
        allowed_paths: vec!["/data/alpha".into()],
        allowed_hosts: vec!["api.alpha.com".into()],
    });

    manager.register_tenant(TenantQuota {
        tenant_id: "tenant-beta".into(),
        max_memory_mb: 64,
        max_cpu_time_ms: 10000,
        max_invocations_per_min: 200,
        max_network_requests: 100,
        allowed_paths: vec!["/data/beta".into(), "/shared".into()],
        allowed_hosts: vec!["api.beta.com".into(), "cdn.example.com".into()],
    });

    println!("tenant-alpha invoke: {:?}", manager.check_invocation("tenant-alpha"));
    println!("tenant-alpha fs /data/alpha/config.json: {:?}", manager.check_fs("tenant-alpha", "/data/alpha/config.json"));
    println!("tenant-alpha fs /data/beta/secret: {:?}", manager.check_fs("tenant-alpha", "/data/beta/secret"));
    println!("tenant-alpha net api.alpha.com: {:?}", manager.check_network("tenant-alpha", "api.alpha.com"));
    println!("tenant-alpha net evil.com: {:?}", manager.check_network("tenant-alpha", "evil.com"));
    println!("tenant-beta net cdn.example.com: {:?}", manager.check_network("tenant-beta", "cdn.example.com"));
}
# Multi-tenant WasmEdge: independent sandbox per tenant
# Tenant Alpha
wasmedge \
  --memory-page-limit 512 \
  --time-limit 5000 \
  --dir /data/alpha:/data/alpha:readonly \
  --allow-host api.alpha.com \
  /plugins/alpha/worker.wasm &

# Tenant Beta
wasmedge \
  --memory-page-limit 1024 \
  --time-limit 10000 \
  --dir /data/beta:/data/beta:readonly \
  --dir /shared:/shared:readonly \
  --allow-host api.beta.com \
  --allow-host cdn.example.com \
  /plugins/beta/worker.wasm &
apiVersion: apps/v1
kind: Deployment
metadata:
  name: wasm-plugin-router
spec:
  replicas: 3
  selector:
    matchLabels:
      app: wasm-plugin-router
  template:
    spec:
      containers:
      - name: router
        image: toolsku/wasm-plugin-router:1.0
        env:
        - name: TENANT_CONFIG
          value: /config/tenants.yaml
        volumeMounts:
        - name: config
          mountPath: /config
        - name: plugins
          mountPath: /plugins
        resources:
          limits:
            cpu: "2"
            memory: "512Mi"
      volumes:
      - name: config
        configMap:
          name: tenant-quotas
      - name: plugins
        persistentVolumeClaim:
          claimName: wasm-plugins-pvc

Pattern 5: Production Security Audit — Vulnerability Scanning and Supply Chain Verification

use serde::{Deserialize, Serialize};
use std::collections::HashMap;

#[derive(Serialize, Deserialize, Clone)]
pub struct VulnerabilityReport {
    pub module_hash: String,
    pub module_size: u64,
    pub scan_time: String,
    pub risk_level: RiskLevel,
    pub findings: Vec<Finding>,
    pub dependencies: Vec<DependencyInfo>,
    pub wasm_features: Vec<WasmFeature>,
}

#[derive(Serialize, Deserialize, Clone, PartialEq)]
pub enum RiskLevel {
    Critical,
    High,
    Medium,
    Low,
    Info,
}

#[derive(Serialize, Deserialize, Clone)]
pub struct Finding {
    pub id: String,
    pub category: FindingCategory,
    pub severity: RiskLevel,
    pub description: String,
    pub recommendation: String,
}

#[derive(Serialize, Deserialize, Clone)]
pub enum FindingCategory {
    MemorySafety,
    WasiPermission,
    SideChannel,
    SupplyChain,
    ResourceExhaustion,
    InformationLeak,
    CodeInjection,
}

#[derive(Serialize, Deserialize, Clone)]
pub struct DependencyInfo {
    pub name: String,
    pub version: String,
    pub hash: String,
    pub source: String,
    pub verified: bool,
}

#[derive(Serialize, Deserialize, Clone)]
pub struct WasmFeature {
    pub name: String,
    pub risk: RiskLevel,
    pub description: String,
}

pub struct WasmSecurityAuditor {
    known_vulnerabilities: HashMap<String, Finding>,
}

impl WasmSecurityAuditor {
    pub fn new() -> Self {
        let mut known_vulnerabilities = HashMap::new();

        known_vulnerabilities.insert("WASM-SEC-001".into(), Finding {
            id: "WASM-SEC-001".into(),
            category: FindingCategory::WasiPermission,
            severity: RiskLevel::Critical,
            description: "Module requests full filesystem access via WASI".into(),
            recommendation: "Restrict WASI to specific directories with --dir flag".into(),
        });

        known_vulnerabilities.insert("WASM-SEC-002".into(), Finding {
            id: "WASM-SEC-002".into(),
            category: FindingCategory::SideChannel,
            severity: RiskLevel::High,
            description: "Module uses high-resolution timer (clock_time_get) - potential side-channel".into(),
            recommendation: "Reduce timer precision or disable high-res clocks in sandbox".into(),
        });

        known_vulnerabilities.insert("WASM-SEC-003".into(), Finding {
            id: "WASM-SEC-003".into(),
            category: FindingCategory::ResourceExhaustion,
            severity: RiskLevel::High,
            description: "Module declares unbounded memory (no maximum)".into(),
            recommendation: "Set memory page limit with --memory-page-limit".into(),
        });

        known_vulnerabilities.insert("WASM-SEC-004".into(), Finding {
            id: "WASM-SEC-004".into(),
            category: FindingCategory::InformationLeak,
            severity: RiskLevel::Medium,
            description: "Module imports environment variables without filtering".into(),
            recommendation: "Use --allow-env to whitelist specific environment variables".into(),
        });

        known_vulnerabilities.insert("WASM-SEC-005".into(), Finding {
            id: "WASM-SEC-005".into(),
            category: FindingCategory::SupplyChain,
            severity: RiskLevel::Critical,
            description: "Module contains unverified third-party dependencies".into(),
            recommendation: "Verify all dependency hashes against registry checksums".into(),
        });

        WasmSecurityAuditor { known_vulnerabilities }
    }

    pub fn audit_module(&self, wasm_path: &str) -> VulnerabilityReport {
        let mut findings = Vec::new();

        let metadata = std::fs::metadata(wasm_path).expect("cannot read module");
        let module_size = metadata.len();

        let module_hash = self.compute_hash(wasm_path);

        let wasm_features = self.analyze_features(wasm_path);
        for feature in &wasm_features {
            if feature.risk == RiskLevel::High || feature.risk == RiskLevel::Critical {
                findings.push(Finding {
                    id: format!("FEATURE-{}", feature.name),
                    category: FindingCategory::WasiPermission,
                    severity: feature.risk.clone(),
                    description: format!("Wasm feature used: {} - {}", feature.name, feature.description),
                    recommendation: "Review if this feature is necessary for the module's function".into(),
                });
            }
        }

        findings.push(Finding {
            id: "WASM-SEC-003".into(),
            category: FindingCategory::ResourceExhaustion,
            severity: RiskLevel::High,
            description: "Always set memory limits in production".into(),
            recommendation: "Use --memory-page-limit to cap memory usage".into(),
        });

        let overall_risk = if findings.iter().any(|f| f.severity == RiskLevel::Critical) {
            RiskLevel::Critical
        } else if findings.iter().any(|f| f.severity == RiskLevel::High) {
            RiskLevel::High
        } else if findings.iter().any(|f| f.severity == RiskLevel::Medium) {
            RiskLevel::Medium
        } else {
            RiskLevel::Low
        };

        VulnerabilityReport {
            module_hash,
            module_size,
            scan_time: chrono::Utc::now().to_rfc3339(),
            risk_level: overall_risk,
            findings,
            dependencies: vec![],
            wasm_features,
        }
    }

    fn compute_hash(&self, path: &str) -> String {
        use std::io::Read;
        let mut file = std::fs::File::open(path).expect("cannot open file");
        let mut data = Vec::new();
        file.read_to_end(&mut data).expect("cannot read file");

        use std::collections::hash_map::DefaultHasher;
        use std::hash::{Hash, Hasher};
        let mut hasher = DefaultHasher::new();
        data.hash(&mut hasher);
        format!("{:016x}", hasher.finish())
    }

    fn analyze_features(&self, _wasm_path: &str) -> Vec<WasmFeature> {
        vec![
            WasmFeature {
                name: "wasi_snapshot_preview1".into(),
                risk: RiskLevel::Medium,
                description: "WASI preview1 filesystem access".into(),
            },
            WasmFeature {
                name: "wasi_http".into(),
                risk: RiskLevel::Medium,
                description: "HTTP outbound requests".into(),
            },
        ]
    }
}

fn main() {
    let auditor = WasmSecurityAuditor::new();

    println!("=== Wasm Security Audit Tool ===");
    println!("Known vulnerability signatures: {}", auditor.known_vulnerabilities.len());

    for (id, finding) in &auditor.known_vulnerabilities {
        println!("[{}] {:?} - {}", id, finding.severity, finding.description);
    }
}
# Wasm module security scanning workflow
# 1. Verify module signature
cosign verify-blob \
  --certificate module.crt \
  --signature module.sig \
  plugin.wasm

# 2. Analyze Wasm module imports
wasm-objdump -x plugin.wasm | grep -E "import|export"

# 3. Check WASI permission requirements
wasm-tools print plugin.wasm | grep -E "wasi_snapshot_preview1|wasi_http"

# 4. Run security audit
cargo run --bin wasm-security-auditor -- plugin.wasm

# 5. Restrictive sandbox testing
wasmedge \
  --memory-page-limit 256 \
  --time-limit 5000 \
  --dir /tmp/sandbox:/tmp/sandbox:readonly \
  plugin.wasm test_function

# 6. CI/CD integration
echo 'wasm-security-audit:
  stage: security
  image: toolsku/wasm-auditor:1.0
  script:
    - wasm-audit --strict ./target/wasm32-wasip1/release/*.wasm
    - wasm-verify --cosign ./target/wasm32-wasip1/release/*.wasm
  rules:
    - changes:
      - "**/*.rs"
      - "**/Cargo.toml"'
name: Wasm Security Audit
on:
  push:
    paths:
      - 'src/**/*.rs'
      - 'Cargo.toml'
      - 'wasm/**'

jobs:
  security-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install Wasm Tools
        run: |
          curl -sSf https://wasmtime.dev/install.sh | bash
          cargo install wasm-tools wasm-objdump

      - name: Build Wasm Module
        run: |
          rustup target add wasm32-wasip1
          cargo build --target wasm32-wasip1 --release

      - name: Verify Module Integrity
        run: |
          sha256sum target/wasm32-wasip1/release/*.wasm > checksums.txt
          cosign verify-blob --certificate cosign.crt \
            --signature cosign.sig \
            target/wasm32-wasip1/release/*.wasm

      - name: Analyze WASI Permissions
        run: |
          for wasm in target/wasm32-wasip1/release/*.wasm; do
            echo "=== $wasm ==="
            wasm-objdump -x "$wasm" | grep "import" || true
            wasm-tools print "$wasm" | grep "wasi" || true
          done

      - name: Sandbox Test
        run: |
          wasmedge \
            --memory-page-limit 256 \
            --time-limit 5000 \
            target/wasm32-wasip1/release/*.wasm test

      - name: Generate Audit Report
        run: |
          cargo run --bin wasm-security-auditor \
            -- target/wasm32-wasip1/release/*.wasm \
            > security-report.json

      - name: Upload Audit Report
        uses: actions/upload-artifact@v4
        with:
          name: wasm-security-report
          path: security-report.json

Pitfall Guide

❌ Pitfall 1: Trusting Wasm "Natural Security" Without Configuring WASI Permissions

# ❌ Wrong: Running WASI module directly, may access current directory and env vars by default
wasmedge plugin.wasm

# ✅ Correct: Explicitly restrict all WASI permissions
wasmedge \
  --dir /app/data:/app/data:readonly \
  --env APP_MODE=production \
  --allow-env APP_MODE \
  --allow-host api.example.com \
  --memory-page-limit 256 \
  --time-limit 5000 \
  plugin.wasm

❌ Pitfall 2: Not Setting Memory Limits on Wasm Modules

# ❌ Wrong: No memory limit, malicious module could exhaust host memory
wasmedge module.wasm

# ✅ Correct: Set memory page limit (64KB per page)
wasmedge --memory-page-limit 256 module.wasm  # 16MB limit

❌ Pitfall 3: Ignoring Side-Channel Attack Risks

// ❌ Wrong: Exposing high-resolution timers
// WASI's clock_time_get can provide nanosecond precision

// ✅ Correct: Reduce timer precision
// WasmEdge: Use --time-limit to limit total execution time
// Custom WASI implementation: Reduce clock_time_get precision to 1ms

❌ Pitfall 4: Multiple Tenants Sharing the Same Wasm Runtime Instance

// ❌ Wrong: All tenants share one Store
// let mut store = Store::new(&engine, ());
// tenant_a_module.invoke(&mut store, ...);
// tenant_b_module.invoke(&mut store, ...);

// ✅ Correct: Each tenant gets an independent Store and resource limits
// let store_a = Store::new(&engine, TenantContext::new("alpha", quota_a));
// let store_b = Store::new(&engine, TenantContext::new("beta", quota_b));

❌ Pitfall 5: Not Verifying Third-Party Wasm Module Sources

# ❌ Wrong: Downloading and running unverified Wasm modules
curl -sL https://example.com/plugin.wasm | wasmedge -

# ✅ Correct: Verify signature before running
curl -sL -o plugin.wasm https://example.com/plugin.wasm
curl -sL -o plugin.wasm.sig https://example.com/plugin.wasm.sig
cosign verify-blob --certificate pub.crt --signature plugin.wasm.sig plugin.wasm
wasmedge --memory-page-limit 256 plugin.wasm

Error Troubleshooting

Error Message Cause Solution
WasmEdge: out of memory Module memory exceeds --memory-page-limit Increase page limit or optimize module memory usage
WasmEdge: time limit exceeded Execution exceeds --time-limit Increase timeout or optimize algorithm complexity
wasm trap: out of bounds memory access Module accesses linear memory out of bounds Check memory access logic, ensure offsets are valid
permission denied: /etc/passwd WASI filesystem permission insufficient Use --dir to mount allowed directories
host not allowed: evil.com Network request blocked by --allow-host Only allow necessary domains
env var not allowed: DATABASE_URL Environment variable filtered by --allow-env Only allow necessary environment variables
module verification failed Module signature verification failed Check signature file and certificate match
table overflow: max 1024 Indirect call table exceeds limit Increase --max-table-size or reduce indirect calls
gas limit exceeded Gas consumption exceeds limit Increase gas limit or optimize computation
sandbox violation: fs write Write attempt on read-only directory Use writeonly or readwrite permission mount

Advanced Optimization

1. Multi-Layer Defense Architecture

defense_layers:
  layer1_wasm_isolation:
    description: "Linear memory isolation, modules cannot access out of bounds"
    mechanism: "Wasm specification enforced"
    bypass_difficulty: "Very high (requires Wasm implementation vulnerability)"

  layer2_wasi_capability:
    description: "Capability model, default deny all system access"
    mechanism: "Explicitly granted permissions"
    bypass_difficulty: "High (requires permission misconfiguration)"

  layer3_resource_limits:
    description: "Memory, CPU, network resource quotas"
    mechanism: "Runtime enforced"
    bypass_difficulty: "High (requires runtime vulnerability)"

  layer4_process_isolation:
    description: "Run Wasm runtime in separate process"
    mechanism: "OS-level isolation"
    bypass_difficulty: "Very high (requires container/process escape)"

  layer5_supply_chain:
    description: "Module signature verification and dependency audit"
    mechanism: "CI/CD enforced checks"
    bypass_difficulty: "Medium (requires compromising signature system)"

2. Custom WASI Implementation — Minimal Privilege Host Functions

use wasmtime::*;
use wasmtime_wasi::{WasiCtxBuilder, WasiCtx};

fn create_minimal_wasi(store: &mut Store<WasiCtx>) -> Result<(), anyhow::Error> {
    let wasi = WasiCtxBuilder::new()
        .inherit_stdout()
        .inherit_stderr()
        .env("APP_MODE", "production")
        .preopened_dir(
            Dir::open_ambient_dir("/app/data", AmbientAuthority::try_new()?)?,
            DirPerms::READ,
            FilePerms::READ,
            "/data",
        )?
        .build();

    *store.data_mut() = wasi;
    Ok(())
}

fn main() -> Result<(), anyhow::Error> {
    let engine = Engine::default();
    let module = Module::from_file(&engine, "plugin.wasm")?;

    let mut linker = Linker::new(&engine);
    wasmtime_wasi::add_to_linker(&mut linker, |cx: &mut WasiCtx| cx)?;

    let wasi = WasiCtxBuilder::new()
        .env("APP_MODE", "production")
        .preopened_dir(
            Dir::open_ambient_dir("/app/data", AmbientAuthority::try_new()?)?,
            DirPerms::READ,
            FilePerms::READ,
            "/data",
        )?
        .build();

    let mut store = Store::new(&engine, wasi);

    let instance = linker.instantiate(&mut store, &module)?;

    let run = instance.get_typed_func::<(), ()>(&mut store, "run")?;
    run.call(&mut store, ())?;

    Ok(())
}

3. Real-Time Security Monitoring

use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::Instant;

pub struct SecurityMonitor {
    memory_violations: AtomicU64,
    timeout_violations: AtomicU64,
    permission_violations: AtomicU64,
    network_violations: AtomicU64,
    total_invocations: AtomicU64,
    start_time: Instant,
}

impl SecurityMonitor {
    pub fn new() -> Self {
        SecurityMonitor {
            memory_violations: AtomicU64::new(0),
            timeout_violations: AtomicU64::new(0),
            permission_violations: AtomicU64::new(0),
            network_violations: AtomicU64::new(0),
            total_invocations: AtomicU64::new(0),
            start_time: Instant::now(),
        }
    }

    pub fn record_invocation(&self) {
        self.total_invocations.fetch_add(1, Ordering::SeqCst);
    }

    pub fn record_memory_violation(&self) {
        self.memory_violations.fetch_add(1, Ordering::SeqCst);
    }

    pub fn record_timeout_violation(&self) {
        self.timeout_violations.fetch_add(1, Ordering::SeqCst);
    }

    pub fn record_permission_violation(&self) {
        self.permission_violations.fetch_add(1, Ordering::SeqCst);
    }

    pub fn record_network_violation(&self) {
        self.network_violations.fetch_add(1, Ordering::SeqCst);
    }

    pub fn report(&self) -> String {
        let uptime = self.start_time.elapsed().as_secs();
        let total = self.total_invocations.load(Ordering::SeqCst);
        let mem_v = self.memory_violations.load(Ordering::SeqCst);
        let timeout_v = self.timeout_violations.load(Ordering::SeqCst);
        let perm_v = self.permission_violations.load(Ordering::SeqCst);
        let net_v = self.network_violations.load(Ordering::SeqCst);

        format!(
            "Security Monitor Report (uptime: {}s)\n\
             Total invocations: {}\n\
             Memory violations: {}\n\
             Timeout violations: {}\n\
             Permission violations: {}\n\
             Network violations: {}\n\
             Violation rate: {:.2}%",
            uptime, total, mem_v, timeout_v, perm_v, net_v,
            if total > 0 { (mem_v + timeout_v + perm_v + net_v) as f64 / total as f64 * 100.0 } else { 0.0 }
        )
    }
}

Comparison Analysis

Dimension Wasm Sandbox Docker Container gVisor Firecracker
Isolation granularity Function-level Process-level Process-level VM-level
Cold start <1ms 300-800ms 1-3s 125ms
Memory overhead 5-30MB 50MB+ 100MB+ 30MB+
Attack surface Wasm instruction set Syscalls Syscalls Syscalls
Escape difficulty Very high Medium High High
Capability model WASI default deny Manual Seccomp Manual configuration Manual configuration
Resource control Memory pages + timeout + Gas Cgroup Cgroup Cgroup + VM
Supply chain audit Module signature + dependency scan Image scan Image scan Image scan
Multi-tenant Native support Extra configuration Extra configuration VM-level isolation
Use case Plugins/Serverless General services Security containers Serverless VM

Conclusion: WebAssembly security sandbox cannot be summarized as "naturally secure" in four words. From linear memory isolation to WASI capability model, from WasmEdge resource limits to multi-tenant quotas, from supply chain auditing to real-time monitoring, each layer is a critical component of defense in depth. In production, you must: default deny all WASI permissions, set memory and timeout limits, verify all third-party module signatures, isolate each tenant in independent sandboxes, and continuously monitor security events. Security is never a one-time configuration — it's a continuous engineering practice.


Try these browser-local tools — no sign-up required →

#Wasm安全#沙箱隔离#WASI#能力模型#WasmEdge#2026#边缘计算