WebAssemblyの「安全神話」を壊す:サンドボックス脱出とメモリ汚染の現実
「Wasm(WebAssembly)を使えばブラウザのサンドボックス内で安全にコードが動く」――そう信じているなら、今日でその認識を改めたほうがいい。
確かにWasmはメモリ安全性を考慮して設計されている。しかし、開発者が「CやRustのメモリ管理」をブラウザという泥沼に持ち込んだ瞬間、その安全神話は崩れ去る。今日は、WebAssemblyにおけるメモリ破壊のリスクと、JavaScript(JS)との境界線で何が起きているのか、現場の最前線から解説する。
1. Wasmが抱える「境界線」の脆弱性
Wasmのサンドボックスは強固だ。しかし、Wasmモジュール単体で完結するアプリは稀で、多くはJSとメモリを共有して動いている。ここで悪用されるのが 「線形メモリ(Linear Memory)」へのアクセス権 だ。
攻撃者は、Wasm内のバッファオーバーフローを突いて、JS側のオブジェクトやDOMを書き換えようとする。もし君のアプリが「信頼できない入力」をそのままWasmのメモリ領域に書き込んでいたら、それはXSSの高度な亜種――「Wasmベースのコード注入」への入り口になる。
具体的な攻撃のシナリオ
1. 入力の汚染: 攻撃者が細工したペイロードをJS経由でWasmのメモリへ渡す。
2. メモリ破壊: Wasm側で境界チェックを怠ったバッファコピー(memcpy等)を実行。
3. 制御フローのハイジャック: スタックや関数ポインタを上書きし、JS側の関数を悪意ある引数で再実行させる。
2. セキュアな実装:防御の黄金律
Wasmの境界で最も重要なのは「JSとWasm間のメモリ分離」と「型変換の厳格化」だ。境界をまたぐ際は、生ポインタを直接渡してはならない。
実装サンプル:安全なデータ受け渡し(JavaScript/Rust)
RustでWasmをコンパイルする際、JS側に渡すデータは「コピー」を基本とし、直接メモリを操作させない設計にする。
[Rust側: src/lib.rs]
use wasm_bindgen::prelude::;
[wasm_bindgen]
pub fn process_data(input: &str) -> String {
// 境界チェック済みの安全な処理
// 生のポインタ操作は避け、WasmBindgenの型安全なラッパーを通す
format!(“Processed: {}”, input)
}
[JavaScript側: app.js]
import init, { process_data } from ‘./pkg/my_wasm_module.js’;
async function run() {
await init();
const userInput = ““;
// 防御策:JS側でサニタイズしてからWasmに渡す
// Wasmがメモリを操作する前に、攻撃の芽を摘む
const sanitizedInput = userInput.replace(/<[^>]>?/gm, ”);
const result = process_data(sanitizedInput);
console.log(result);
}
3. インフラレベルでの防御:CSPの要塞化
Wasmを悪用した攻撃を防ぐには、アプリケーションコードだけでなく、ブラウザのポリシーで「何ができるか」を縛り上げる必要がある。特に Content-Security-Policy (CSP) は必須だ。
推奨するCSP設定(Nginx/Webサーバー)
Wasmの実行を許可しつつ、意図しない外部スクリプトの混入を許さない厳格な設定だ。
Wasmの実行を許可し、eval()やインラインスクリプトを禁止する
add_header Content-Security-Policy “default-src ‘self’; script-src ‘self’ ‘wasm-unsafe-eval’; object-src ‘none’; base-uri ‘self’;”;
'wasm-unsafe-eval': Wasmの実行を許可するためのフラグ。これがないとWasm自体が動き出さない。object-src 'none': プラグインによる攻撃を防ぐために必須。script-src 'self': CDN経由のスクリプト注入を許さない鉄壁の構成。
4. 現場のセキュリティ担当者からのアドバイス
「Wasmだから大丈夫」という思考停止が、インシデントを生む。以下の3点を常に意識してくれ。
1. 入力の検証は多層で: JS側でのサニタイズを「フロントエンドの気休め」と思わず、Wasmへ渡す前の最後の砦として実装せよ。
2. メモリ境界を疑え: C/C++からWasmへコンパイルする場合、Emscriptenのメモリ保護オプション(ALLOW_MEMORY_GROWTH=0 など)を有効にし、必要最小限のメモリしか与えないこと。
3. 依存関係の監査: cargo audit や npm audit をCI/CDパイプラインに組み込み、Wasmのライブラリに既知の脆弱性(メモリ破壊など)がないか常に監視しろ。
Wasmは強力な武器だが、使い方を誤れば自分自身を切り裂く刃になる。我々エンジニアの仕事は、その刃を「安全に扱うための鞘」を作ることだ。
コードを書くとき、一瞬手を止めて考えてほしい。「このデータが境界を越えるとき、誰がその整合性を保証するのか?」。その問いに対する答えが、君のシステムのセキュリティレベルを決定づける。
コメント