WebAssembly安全沙箱實戰:從隔離到能力模型的5種生產模式

边缘计算

WebAssembly安全沙箱實戰:從隔離到能力模型的5種生產模式

2026年,WebAssembly已經從瀏覽器走向了邊緣運算、Serverless、插件系統等核心生產場景。但「安全」這兩個字,從來不是Wasm線性記憶體模型天然就能包辦的。一個未設定WASI權限的Wasm模組,可能直接讀取宿主機的環境變數;一個沒有記憶體上限的插件,可能把宿主程序拖入OOM深淵;一個未經驗證的第三方Wasm元件,可能藏著供應鏈攻擊的木馬。本文從5種生產模式出發,覆蓋隔離、能力、設定、架構、審計,幫你建構真正安全的Wasm執行時期。

安全威脅 容器逃逸 Wasm沙箱逃逸
攻擊面 系統呼叫+Namespace 線性記憶體+WASI
歷史CVE CVE-2024-21626等 極少(理論存在)
縱深防禦 Seccomp+AppArmor 能力模型+記憶體限制+超時
最小權限 需手動設定 WASI預設拒絕
審計粒度 程序級 函式級

核心概念

概念 全稱 說明
Linear Memory 線性記憶體 Wasm模組的記憶體模型,連續位元組陣列,模組無法越界存取,是沙箱隔離的基石
WASI WebAssembly System Interface Wasm存取作業系統的標準介面,基於能力模型,預設拒絕所有系統存取
Capability Model 能力模型 安全模型,權限必須被顯式授予,而非預設擁有後撤銷
Sandbox 沙箱 隔離執行環境,Wasm模組在沙箱中執行,無法直接存取宿主資源
WasmEdge 輕量級Wasm執行時期,支援AOT編譯、WASI-NN、資源限制等安全特性
WASI-NN WASI Neural Network Wasm進行AI推理的標準介面,透過能力模型控制模型存取
Supply Chain Attack 供應鏈攻擊 透過注入惡意程式碼到第三方依賴中進行攻擊,Wasm場景同樣存在

問題分析:Wasm安全為什麼不能只靠「天然隔離」?

1. 線性記憶體隔離不等於系統級隔離

Wasm的線性記憶體模型確實防止了模組越界讀寫宿主記憶體,但這只是記憶體層面的隔離。透過WASI,Wasm模組可以存取檔案系統、網路、環境變數——如果WASI權限設定不當,「天然隔離」就是一句空話。

2. WASI預設權限過於寬鬆

很多執行時期預設允許WASI模組存取當前工作目錄、環境變數、網路。在生產環境中,一個惡意Wasm模組可以透過WASI讀取/etc/passwd、發起網路請求外洩資料、甚至透過poll_oneoff進行側頻道攻擊。

3. 側頻道攻擊真實存在

Spectre-style攻擊在Wasm中同樣可能:透過高精度計時器(performance.now或WASI的clock_time_get),惡意模組可以推測宿主程序的記憶體佈局。2025年的研究已經證明了Wasm側頻道的可行性。

4. 供應鏈攻擊無孔不入

你的Wasm模組依賴了10個第三方crate,其中一個被惡意篡改——這在Rust生態中已經發生過多次(如rustdecimal事件)。Wasm模組編譯後的二進位更難審計,供應鏈攻擊的隱蔽性更強。

5. 多租戶場景資源隔離缺失

在插件系統或Serverless平台中,多個Wasm模組共享同一個執行時期程序。如果沒有資源配額(記憶體上限、CPU時間、呼叫次數),一個惡意或失控的模組可以耗盡所有資源,影響其他租戶。


分步實操

模式1:Wasm線性記憶體隔離——記憶體邊界與逃逸防護

;; 線性記憶體宣告:1頁初始(64KB),最大256頁(16MB)
(module
  (memory (export "memory") 1 256)

  ;; 安全的記憶體存取:帶邊界檢查
  (func (export "safe_read") (param $offset i32) (param $len i32) (result i32)
    (local $end i32)
    ;; 計算結束位置
    (local.set $end (i32.add (local.get $offset) (local.get $len)))
    ;; 邊界檢查:確保不超出記憶體範圍
    (if (i32.gt_u (local.get $end) (i32.mul (memory.size) (i32.const 65536)))
      (then (return (i32.const 0)))  ;; 傳回錯誤碼
    )
    ;; 安全讀取
    (i32.load (local.get $offset))
  )

  ;; 安全的記憶體寫入:帶邊界檢查
  (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)  ;; 成功
  )
)
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());
    }
}
# 編譯WAT到Wasm
wat2wasm safe_memory.wat -o safe_memory.wasm

# 驗證記憶體邊界
wasm-validate safe_memory.wasm

# 使用WasmEdge執行並限制記憶體
wasmedge --memory-page-limit 256 safe_memory.wasm

# 使用Wasmtime執行並限制記憶體
wasmtime run --max-wasm-stack 512000 --wasm-max-memory 16 safe_memory.wasm

模式2:WASI能力模型安全——檔案系統、網路、環境變數權限控制

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!("讀取 /app/data/config.json: {}", caps.check_fs_read("/app/data/config.json"));
    println!("讀取 /etc/passwd: {}", caps.check_fs_read("/etc/passwd"));
    println!("寫入 /app/output/result.json: {}", caps.check_fs_write("/app/output/result.json"));
    println!("寫入 /tmp/malware: {}", caps.check_fs_write("/tmp/malware"));
    println!("存取 api.example.com:443: {}", caps.check_net("api.example.com", 443));
    println!("存取 evil.com:443: {}", caps.check_net("evil.com", 443));
    println!("讀取 APP_MODE: {}", caps.check_env("APP_MODE"));
    println!("讀取 DATABASE_URL: {}", caps.check_env("DATABASE_URL"));
}
# WasmEdge:最小權限執行WASI模組
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權限設定
wasmtime run \
  --wasi preview2 \
  --dir /app/data::/app/data:readonly \
  --dir /app/output::/app/output:writeonly \
  --env APP_MODE=production \
  module.wasm

# Wasmer:限制網路和檔案系統
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

模式3:WasmEdge沙箱設定——資源限制、超時、記憶體上限

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嚴格沙箱:16MB記憶體 + 5秒超時
wasmedge \
  --memory-page-limit 256 \
  --time-limit 5000 \
  --allow-host api.example.com \
  --env APP_MODE=production \
  /app/plugin.wasm

# WasmEdge中等沙箱:32MB記憶體 + 30秒超時
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寬鬆沙箱:128MB記憶體 + 5分鐘超時(僅限可信模組)
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

模式4:插件隔離架構——多租戶資源配額

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"));
}
# 多租戶WasmEdge執行:每個租戶獨立沙箱
# 租戶Alpha
wasmedge \
  --memory-page-limit 512 \
  --time-limit 5000 \
  --dir /data/alpha:/data/alpha:readonly \
  --allow-host api.alpha.com \
  /plugins/alpha/worker.wasm &

# 租戶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

模式5:生產安全審計——漏洞掃描與供應鏈驗證

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模組安全掃描工作流
# 1. 驗證模組簽名
cosign verify-blob \
  --certificate module.crt \
  --signature module.sig \
  plugin.wasm

# 2. 分析Wasm模組匯入
wasm-objdump -x plugin.wasm | grep -E "import|export"

# 3. 檢查WASI權限需求
wasm-tools print plugin.wasm | grep -E "wasi_snapshot_preview1|wasi_http"

# 4. 執行安全審計
cargo run --bin wasm-security-auditor -- plugin.wasm

# 5. 限制性沙箱測試
wasmedge \
  --memory-page-limit 256 \
  --time-limit 5000 \
  --dir /tmp/sandbox:/tmp/sandbox:readonly \
  plugin.wasm test_function

# 6. CI/CD整合
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

避坑指南

❌ 坑1:信任Wasm「天然安全」不設定WASI權限

# ❌ 錯誤:直接執行WASI模組,預設可能存取當前目錄和環境變數
wasmedge plugin.wasm

# ✅ 正確:顯式限制所有WASI權限
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

❌ 坑2:Wasm模組記憶體不設上限

# ❌ 錯誤:不限制記憶體,惡意模組可能耗盡宿主記憶體
wasmedge module.wasm

# ✅ 正確:設定記憶體頁上限(每頁64KB)
wasmedge --memory-page-limit 256 module.wasm  # 16MB上限

❌ 坑3:忽略側頻道攻擊風險

// ❌ 錯誤:暴露高精度計時器
// WASI的clock_time_get可以提供奈秒級精度

// ✅ 正確:降低計時器精度
// WasmEdge:使用--time-limit限制總執行時間
// 自訂WASI實作:將clock_time_get精度降低到1ms

❌ 坑4:多租戶共享同一Wasm執行時期實例

// ❌ 錯誤:所有租戶共享一個Store
// let mut store = Store::new(&engine, ());
// tenant_a_module.invoke(&mut store, ...);
// tenant_b_module.invoke(&mut store, ...);

// ✅ 正確:每個租戶獨立的Store和資源限制
// let store_a = Store::new(&engine, TenantContext::new("alpha", quota_a));
// let store_b = Store::new(&engine, TenantContext::new("beta", quota_b));

❌ 坑5:不驗證第三方Wasm模組來源

# ❌ 錯誤:直接下載執行未驗證的Wasm模組
curl -sL https://example.com/plugin.wasm | wasmedge -

# ✅ 正確:驗證簽名後再執行
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

報錯排查

報錯資訊 原因 解決方法
WasmEdge: out of memory 模組記憶體超出--memory-page-limit 增大頁限制或最佳化模組記憶體使用
WasmEdge: time limit exceeded 執行超出--time-limit 增大超時或最佳化演算法複雜度
wasm trap: out of bounds memory access 模組越界存取線性記憶體 檢查記憶體存取邏輯,確保偏移量合法
permission denied: /etc/passwd WASI檔案系統權限不足 使用--dir掛載允許的目錄
host not allowed: evil.com 網路請求被--allow-host拒絕 只允許必要的域名
env var not allowed: DATABASE_URL 環境變數被--allow-env過濾 只允許必要的環境變數
module verification failed 模組簽名驗證失敗 檢查簽名檔案和憑證是否匹配
table overflow: max 1024 間接呼叫表超限 增大--max-table-size或減少間接呼叫
gas limit exceeded Gas消耗超出限制 增大gas限制或最佳化計算邏輯
sandbox violation: fs write 唯讀目錄嘗試寫入 使用writeonlyreadwrite權限掛載

進階最佳化

1. 多層防禦架構

defense_layers:
  layer1_wasm_isolation:
    description: "線性記憶體隔離,模組無法越界存取"
    mechanism: "Wasm規範強制執行"
    bypass_difficulty: "極高(需Wasm實作漏洞)"

  layer2_wasi_capability:
    description: "能力模型,預設拒絕所有系統存取"
    mechanism: "顯式授予權限"
    bypass_difficulty: "高(需權限設定錯誤)"

  layer3_resource_limits:
    description: "記憶體、CPU、網路資源配額"
    mechanism: "執行時期強制執行"
    bypass_difficulty: "高(需執行時期漏洞)"

  layer4_process_isolation:
    description: "獨立程序執行Wasm執行時期"
    mechanism: "OS級隔離"
    bypass_difficulty: "極高(需容器/程序逃逸)"

  layer5_supply_chain:
    description: "模組簽名驗證和依賴審計"
    mechanism: "CI/CD強制檢查"
    bypass_difficulty: "中(需攻破簽名體系)"

2. 自訂WASI實作——最小權限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. 即時安全監控

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 }
        )
    }
}

對比分析

維度 Wasm沙箱 Docker容器 gVisor Firecracker
隔離粒度 函式級 程序級 程序級 VM級
冷啟動 <1ms 300-800ms 1-3s 125ms
記憶體開銷 5-30MB 50MB+ 100MB+ 30MB+
攻擊面 Wasm指令集 系統呼叫 系統呼叫 系統呼叫
逃逸難度 極高
能力模型 WASI預設拒絕 需手動Seccomp 需手動設定 需手動設定
資源控制 記憶體頁+超時+Gas Cgroup Cgroup Cgroup+VM
供應鏈審計 模組簽名+依賴掃描 映像掃描 映像掃描 映像掃描
多租戶 天然支援 需額外設定 需額外設定 VM級隔離
適用場景 插件/Serverless 通用服務 安全容器 Serverless VM

總結:WebAssembly安全沙箱不是「天然安全」四個字就能概括的。從線性記憶體隔離到WASI能力模型,從WasmEdge資源限制到多租戶配額,從供應鏈審計到即時監控,每一層都是縱深防禦的關鍵一環。生產環境中,你必須做到:預設拒絕所有WASI權限、設定記憶體和超時上限、驗證所有第三方模組簽名、每個租戶獨立沙箱、持續監控安全事件。安全從來不是一次性的設定,而是持續的工程實踐。


線上工具推薦

本站提供瀏覽器本地工具,免註冊即可試用 →

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