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がOSにアクセスするための標準インターフェース。ケーパビリティモデルに基づき、デフォルトですべてのシステムアクセスを拒否 |
| 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スタイルの攻撃はWasmでも可能です。高精度タイマー(performance.nowやWASIのclock_time_get)を通じて、悪意のあるモジュールはホストプロセスのメモリレイアウトを推測できます。2025年の研究は、Wasmサイドチャネルの実現可能性をすでに証明しています。
4. サプライチェーン攻撃は至る所に存在
Wasmモジュールが10個のサードパーティcrateに依存しており、そのうち1つが悪意を持って改ざんされている——これは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
# ✅ 正しい:メモリページ上限を設定(1ページあたり64KB)
wasmedge --memory-page-limit 256 module.wasm # 16MB上限
❌ 落とし穴3:サイドチャネル攻撃リスクを無視する
// ❌ 間違い:高精度タイマーを公開
// WASIのclock_time_getはナノ秒精度を提供可能
// ✅ 正しい:タイマー精度を低下させる
// WasmEdge: --time-limitで総実行時間を制限
// カスタムWASI実装:clock_time_getの精度を1msに低下
❌ 落とし穴4:複数テナントが同じWasmランタイムインスタンスを共有する
// ❌ 間違い:すべてのテナントが1つの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 |
読み取り専用ディレクトリへの書き込み試行 | writeonlyまたはreadwrite権限でマウント |
高度な最適化
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実装——最小権限ホスト関数
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セキュリティサンドボックスは「天然に安全」という4文字で要約できるものではありません。線形メモリ隔離からWASIケーパビリティモデル、WasmEdgeリソース制限からマルチテナントクォータ、サプライチェーン監査からリアルタイムモニタリングまで、各層が多層防御の重要な構成要素です。プロダクションでは、すべてのWASI権限をデフォルトで拒否し、メモリとタイムアウトの制限を設定し、すべてのサードパーティモジュールの署名を検証し、各テナントを独立したサンドボックスに隔離し、セキュリティイベントを継続的に監視しなければなりません。セキュリティは決して一度きりの設定ではなく、継続的なエンジニアリングプラクティスなのです。
オンラインツール推奨
- JSONデータフォーマッター:/ja/json/format
- ハッシュ計算:/ja/encode/hash
- Base64エンコーダー/デコーダー:/ja/encode/base64
ブラウザローカルツールを無料で試す →