DOMベースXSSの「泥沼」から脱却せよ:Trusted Typesによる根本的排除のアーキテクチャ
現場でインシデントレスポンスに当たっていると、「XSS対策は終わった」と豪語するテックリードほど、DOMベースの脆弱性で足元をすくわれる瞬間を何度も見てきた。
サニタイズ用のライブラリを挟み、innerHTMLを避ける。教科書的な対策は誰もが知っている。しかし、開発者が増え、レガシーなサードパーティ製ライブラリが混入し、動的なフロントエンドの複雑性が増した現代において、性善説に基づいた「コーディング規約」だけでDOMベースXSSを防ぐのは、もはや神話に近い。
我々が今、真剣に向き合うべきは、「脆弱性を生み出さないコード」ではなく、「脆弱なコードが実行不可能であるとブラウザが強制するアーキテクチャ」だ。その最適解こそが、Trusted Types APIである。
—
1. なぜ「シンク」の監視だけでは不十分なのか
DOMベースXSSの本質は、信頼できないソース(location.hashやURLSearchParamsなど)から取得したデータが、DOMのシンク(innerHTML, outerHTML, document.write(), eval()など)に直接到達することにある。
従来の対策は、文字列がシンクに入る「直前」でサニタイズを行うというものだった。だが、これは「穴の開いたバケツをテープで塞ぎ続ける」作業だ。新しいライブラリが導入されれば、その監視網をすり抜けるコードが混入するリスクは常にゼロにはならない。
Trusted Typesは、このパラダイムを逆転させる。「文字列そのものをシンクに渡すことを禁止し、型安全なオブジェクトのみを受け入れる」という、ブラウザレベルの実行時強制(Runtime Enforcement)だ。
—
2. Trusted Typesのアーキテクチャ実装
Trusted Typesを有効にすると、ブラウザは危険なシンクに対して「文字列」の受け渡しを拒否し、TypeErrorを投げるようになる。開発者は、あらかじめ定義した「ポリシー」を通して文字列をラップし、信頼されたオブジェクト(TrustedHTML, TrustedScript, TrustedScriptURL)に変換しなければならない。
CSPによる導入(ここから始める)
まず、アプリケーションのHTTPヘッダーでポリシーを強制する。
CSPでTrusted Typesを有効化する
‘default’ポリシーは必須ではないが、作成したポリシー名を指定する必要がある
Content-Security-Policy: require-trusted-types-for ‘script’; trusted-types myPolicy;
ポリシーの実装例
フロントエンド側のコードで、信頼の境界を定義する。ここが唯一の「安全なゲート」となる。
// ポリシーの定義:ここで文字列が安全であると保証するロジックを実装
const policy = trustedTypes.createPolicy(‘myPolicy’, {
createHTML: (input) => {
// ここでDOMPurify等を使用して、厳格にサニタイズを行う
// 戻り値は自動的に TrustedHTML オブジェクトとしてラップされる
return DOMPurify.sanitize(input);
},
createScriptURL: (input) => {
// 動的なスクリプトロードのホワイトリスト化
if (input.startsWith(‘https://trusted.cdn.com/’)) {
return input;
}
throw new Error(‘許可されていないスクリプトソースです’);
}
});
// 実行:文字列を直接入れるとブラウザが例外を投げる
const untrustedData = ‘‘;
// NG: throw TypeError: Failed to set the ‘innerHTML’ property
// element.innerHTML = untrustedData;
// OK: ポリシーを通したオブジェクトのみが受け入れられる
element.innerHTML = policy.createHTML(untrustedData);
—
3. 実務的な移行戦略:いきなり「Fail-Closed」は禁物
既存の大規模アプリケーションで、いきなりTrusted Typesを強制すると、ほぼ確実にシステムは停止する。現場の知見として推奨するのは、「Report Onlyモードによる監査」から始めることだ。
ステップ1:監査モードの運用
HTTPヘッダーに Content-Security-Policy-Report-Only を設定し、違反をログに流し続ける。
Content-Security-Policy-Report-Only: require-trusted-types-for ‘script’; report-uri /csp-violation-report
ステップ2:違反の分類とリファクタリング
ログには「どのシンクに」「どんな文字列が」渡ろうとしたかが詳細に出力される。ここで、意図的に危険な処理をしている箇所と、単に開発者の不注意で放置されている箇所を分類する。重要なのは、「サニタイズできていないコード」を修正するのではなく、「サニタイズを強制するポリシーに置き換える」ことだ。
ステップ3:漸進的な強制
特定モジュールから順次、require-trusted-types-forを本番適用していく。全てのレガシーコードを直す必要はない。ポリシー側で「レガシー用の許可リスト」を用意し、徐々に範囲を絞り込むのが、組織として最も摩擦が少ない。
—
4. チーフホワイトハッカーの視点:アーキテクチャの真価
Trusted Typesを導入する真の価値は、XSSを防ぐことだけではない。「セキュリティチームがフロントエンドの『信頼の境界』を一元管理できる」という点にある。
攻撃者は、アプリケーションの脆弱なエンドポイントを探す際、そのコードの複雑性を突く。しかし、Trusted Typesがあれば、開発者がどれほど雑なコードを書こうと、ブラウザのメモリ・DOM操作レベルで「信頼できないソース」は弾かれる。これは、プロンプトインジェクションに対する入力値のガードレイル設計と本質的に同じ思想だ。
現代のサイバー防衛は、個別の脆弱性をパッチする「対症療法」から、「脆弱性が存在しても実行できない環境」を作る「アーキテクチャの防壁」へとシフトしている。Trusted Typesはその強力な一翼を担うものだ。
もし貴方のチームが、まだDOMベースXSSの検知にgrepや静的解析ツールだけを使っているなら、それは「ザルで水を汲む」ようなものだ。ブラウザの仕様そのものを味方につけ、防御のレイヤーを一段深めてほしい。それが、世界最高峰のセキュリティを構築するための第一歩だ。
コメント