DOMベースXSSの「最後の砦」:Trusted Typesでブラウザを強制的にセキュアにする方法
現場のエンジニア諸君、お疲れ様。今日もどこかで脆弱性スキャンと格闘していることだろう。
さて、クロスサイトスクリプティング(XSS)。「今さらXSSか?」と思うかもしれないが、モダンなフロントエンドフレームワークを使っていれば安全だという幻想は捨てた方がいい。たとえReactやVueを使っていても、dangerouslySetInnerHTMLやv-html、あるいはサードパーティ製のライブラリがDOMを直接操作する箇所で、いとも簡単にDOMベースXSSは発生する。
今日解説するのは、攻撃者がどれだけ洗練されたペイロードを投げ込もうと、ブラウザのレベルで実行を阻止する「Trusted Types」という強力な武器だ。これは単なるお作法ではなく、君たちのアプリケーションを守る「強制力」になる。
—
1. なぜ「Trusted Types」が必要なのか?
従来のXSS対策は、サーバー側での入力バリデーションや、HTMLエスケープが主役だった。しかし、DOMベースXSSはクライアントサイドのJavaScriptだけで完結する。攻撃者のスクリプトがinnerHTMLやdocument.writeといった「シンク(危険な実行点)」に到達した時点で、ゲームオーバーだ。
Trusted Typesは、「文字列をそのままシンクに流し込むことを禁止し、特定の『ポリシー』を通したオブジェクトしか受け付けない」という制約をブラウザに課す仕組みだ。
攻撃者視点の盲点
攻撃者は、URLパラメータやlocalStorageから拾った制御不能な文字列を、DOM操作関数に流し込む。これまでの防御策は「文字列をサニタイズする」という努力目標に依存していたが、Trusted Typesは「サニタイズされていない文字列は、そもそもブラウザが実行を拒否する」という強制力を提供する。
—
2. 実装のステップ:ポリシーの定義
まず、ブラウザに対して「どんな文字列なら信頼できるか」を定義するポリシーを作成する。以下はJavaScriptでの実装例だ。
// CSPでTrusted Typesを有効化すると、このポリシーを通さない操作はすべてブロックされる
if (window.trustedTypes && trustedTypes.createPolicy) {
const policy = trustedTypes.createPolicy(‘myAppPolicy’, {
// createHTMLメソッドを定義する
createHTML: (input) => {
// ここでDOMPurifyなどの信頼できるライブラリを挟むのが定石
// 本来ならここでサニタイズ処理を行う
return DOMPurify.sanitize(input);
}
});
// 以降、innerHTMLに代入する際は必ずこのポリシーを通す必要がある
const el = document.getElementById(‘content’);
const untrustedInput = new URLSearchParams(window.location.search).get(‘name’);
// 修正前: el.innerHTML = untrustedInput; // これだとブラウザがエラーを吐いて停止する
// 修正後: ポリシーを通したオブジェクトを渡す
el.innerHTML = policy.createHTML(untrustedInput);
}
—
3. CSPによる強制力の適用
コードを書いても、攻撃者が「古いブラウザ」や「設定を無視するスクリプト」を送り込んでは意味がない。HTTPレスポンスヘッダーで、ブラウザに対してTrusted Typesの使用を義務付ける必要がある。
Nginxでの設定例
サーバー側でCSPを強制する
require-trusted-types-for ‘script’ を追加することで、
DOM操作関数への文字列代入を即座にブロックする
add_header Content-Security-Policy “default-src ‘self’; require-trusted-types-for ‘script’;”;
これを入れておけば、開発者がうっかりelement.innerHTML = taintedStringと書いても、ブラウザがコンソールに違反ログを吐き出し、実行を阻止してくれる。これが最強の防波堤だ。
—
4. 現場で導入する際の「泥臭い」注意点
理想論だけでは現場は回らない。導入にあたっては以下の点に注意してほしい。
- 段階的導入(Report-Onlyモード):
いきなり強制適用すると、レガシーなコードが全滅する可能性がある。まずは Content-Security-Policy-Report-Only を使い、どの箇所がポリシーに違反しているかを監視ログ(Sentry等)で収集しよう。
- ライブラリとの競合:
古いjQueryプラグインや、DOM操作を多用するレガシーコードはTrusted Typesと相性が悪い。リファクタリングの優先順位をつける指標として利用するのが賢いやり方だ。
- DOMPurifyとの併用:
Trusted Typesは「何を信頼するか」を定義する器に過ぎない。中身のサニタイズには、必ず実績のあるDOMPurifyのようなライブラリをポリシー内で呼び出すこと。自作の正規表現置換で済ませようとしてはいけない。
—
最後に:防御は「性悪説」で考えろ
セキュリティの専門家として言わせてもらうと、開発者が書くコードは「いつか必ず間違える」前提で設計すべきだ。Trusted Typesは、その「間違え」が「致命的なセキュリティインシデント」に発展するのを、ブラウザの力を使って物理的に断ち切る手段だ。
「運用が面倒」と考えるか、「これさえ入れておけば、DOMベースXSSの悪夢から解放される」と考えるか。君たちのアプリケーションの堅牢性は、この一歩で大きく変わる。
さあ、まずは自分の管理しているアプリケーションのCSPを確認するところから始めてみよう。質問があればいつでも聞く。健闘を祈る。
コメント