【実務・中級編】WebAssembly(Wasm)におけるメモリ破壊とサンドボックス脱出の脅威 – アプリケーションセキュリティ & 安全な開発防御ガイド

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は強力な武器だが、使い方を誤れば自分自身を切り裂く刃になる。我々エンジニアの仕事は、その刃を「安全に扱うための鞘」を作ることだ。

コードを書くとき、一瞬手を止めて考えてほしい。「このデータが境界を越えるとき、誰がその整合性を保証するのか?」。その問いに対する答えが、君のシステムのセキュリティレベルを決定づける。

コメント

タイトルとURLをコピーしました