XSS — 他人のブラウザで悪いスクリプトを動かす攻撃

概要 — まず全体をつかむ

中級では「種類は違っても根本原因は一つ=出力の瞬間に文脈に合った変換をしていないこと」を見ました。上級では、DOM型を精密に(sourceとsink)・文脈別エンコードの実際・CSPとTrusted Types・なぜ入力フィルタでは防ぎきれないかを掘り下げます。

詳細 — 1段階ずつ追う

ソースとシンク — DOM型XSSを精密に見る

DOM型XSSは、サーバを介さずブラウザ内だけで完結します。鍵になるのは2つの地点です。

  • source(ソース)※1 — 攻撃者が値を注ぎ込める入口。location.hashlocation.searchdocument.referrerpostMessage で受け取ったデータ・localStorage など
  • sink(シンク)※2 — その値が「コードとして解釈されうる出口」。innerHTMLouterHTMLdocument.writeevalsetTimeout(文字列渡し)・setAttributehrefonclick など)・location への代入

sourceからsinkへ、途中でエスケープされないまま値が流れると発火します。ここで大事なのは、サーバの応答HTMLには危険な文字列が一度も現れないことがあるという点です。

アニメーション『XSSの流れ(仕込み→表示→実行)』を開く
XSS ― 他人のブラウザで、攻撃者のスクリプトが動く投稿表示① 攻撃者悪意のある投稿者投稿欄・コメント欄などに「悪いスクリプト」を紛れ込ませて送信する例: <script>…盗む…</script>② サイトエスケープを忘れた受け取った文字列をそのまま他の利用者のページに表示してしまう=スクリプトが混ざる③ 被害者のブラウザ普通に見ただけページを開いた瞬間にそのスクリプトが実行→ Cookie・セッションを盗む/勝手に操作する🛡 防御出力を文脈に応じてエスケープ(HTML/属性/URL/JSで変換)+ CSP で、許可していないスクリプトの実行をブラウザ側で止めるひとことで言うと ― 他人のブラウザで悪いJSを動かす攻撃

だからサーバ側の出力エスケープだけでは素通りします。防御はsinkの手前、つまりブラウザ内で「HTMLとして解釈させない代入(textContent など)」や「サニタイズ」を置くことになります。反射型・格納型が「サーバの出力の問題」なのに対し、DOM型は「ブラウザ内の出力の問題」——同じXSSでも守る場所が違います。

登場人物メモ

  • ※1 source — 攻撃者が制御できる値の入口(location.hash などブラウザが持つデータ)
  • ※2 sink — 値がコードやHTMLとして解釈されうる出口(innerHTMLeval など)
やさしく言うと(中級

XSS は「信頼できない入力が、エスケープされずに HTML・JS・URL へ出力される」ときに成立します。経由する場所で種類が分かれます。

  • 反射型 — リクエストに含めた値が、その応答ページにそのまま出る
  • 格納型 — 保存された値(コメントなど)が、後で別の利用者の画面に出る
  • DOM型 — サーバを介さず、ブラウザ内の JavaScript が値を危険な形で DOM へ書き込む
アニメーション『XSSの流れ(仕込み→表示→実行)』を開く
XSS ― 他人のブラウザで、攻撃者のスクリプトが動く投稿表示① 攻撃者悪意のある投稿者投稿欄・コメント欄などに「悪いスクリプト」を紛れ込ませて送信する例: <script>…盗む…</script>② サイトエスケープを忘れた受け取った文字列をそのまま他の利用者のページに表示してしまう=スクリプトが混ざる③ 被害者のブラウザ普通に見ただけページを開いた瞬間にそのスクリプトが実行→ Cookie・セッションを盗む/勝手に操作する🛡 防御出力を文脈に応じてエスケープ(HTML/属性/URL/JSで変換)+ CSP で、許可していないスクリプトの実行をブラウザ側で止めるひとことで言うと ― 他人のブラウザで悪いJSを動かす攻撃

種類は違っても根本原因は一つ——出力の瞬間に、文脈に合った変換をしていないこと。結果として Cookie(ログイン状態)を盗まれる、勝手に操作される、などの被害が起きます。

格納型・反射型の内部差とmXSS

反射型と格納型は「どこを経由して値がHTMLに混ざるか」の違いでした。上級では、その内部差が守り方に効くことを押さえます。

  • 反射型 — 値はリクエストごとに一度だけ応答へ反射する。被害は「その細工URLを踏んだ人」に限られ、誘導(フィッシング)が前提になる
  • 格納型 — 値がサーバに保存され、閲覧した全員に繰り返し配られる。誘導が要らず、被害範囲が桁違いに広い(ワーム化もありうる)

さらに厄介なのが mutation XSS(mXSS) です。これは、いったん安全に見えた文字列が、ブラウザのHTMLパーサが「見た目を正す」過程で危険なタグに化ける現象です。

  • サニタイズ処理は「ある解釈」で無害と判断した
  • ところが innerHTML に入れた瞬間、ブラウザが不正なネストや文字を別のツリーに再構築し、無害だったはずの断片が実行可能なタグに変わる
  • 教訓は「文字列として安全」と「DOMに入れても安全」は別物、ということ。だから信頼できるサニタイザに任せる

文脈別の出力エンコードとサニタイズ

XSS対策の核心は「入力の混入ではなく、出力の文脈で防ぐ」ことでした。上級では、その「文脈」を具体化します。値を出す先ごとに、危険な文字も脱出の仕方も違うからです。

  • HTML本文< > & を実体参照へ。タグの開始を許さない
  • HTML属性 — 属性を囲む引用符と、引用符を省いたときの空白などを無害化。属性の脱出を防ぐ
  • JavaScript文字列 — 文字列リテラルを抜ける記号(引用符・バックスラッシュ・改行)を処理。値をコードとして生成しないため、そもそもJSにデータを直書きしない設計が上策
  • URL — スキームを検証(javascript: を弾く)し、パーセントエンコード。属性の hrefsrc は要注意
  • CSS — スタイル値へ生の入力を入れない。expression 相当や url() 経由の悪用を避ける

自由なHTMLをどうしても許したい(リッチテキストなど)場合は、自作エスケープではなく DOMPurify のような実績あるサニタイザで「許可タグ・属性の許可リスト方式」に通します。ここでmXSSまで面倒を見てくれるのが、既製サニタイザを使う理由です。

アニメーション『多層防御(層で守る)』を開く
Webのセキュリティ=層で守る(多層防御)👁 運用(ログ・監視)🛡 エッジWAF・レート制限で入口をふるいにかける🔒 通信TLS/HTTPS で暗号化(盗み見・改ざんを防ぐ)🔑 アプリ認証・認可・入出力の扱い🗄 データ保存時暗号・最小権限リクエスト各層を1つずつ通過1枚破られても、次の層で止める ── だから重ねて守る

なぜ入力側で一律に消さないのか。値は保存や再利用の途中では無害で、危険になるのは「特定の文脈へ出す瞬間」だけだからです。入口で削ると本来正しいデータ(a < b のような文字列)まで壊し、しかも別の出口では守れません。詳しくは 入力バリデーションWebの中のセキュリティ を参照。

やさしく言うと(中級

守りは出力側に置きます。

  1. 文脈別の出力エスケープ — HTML本文・HTML属性・JS・URL では必要な変換が違う。出す先に合った規則で「ただの文字」に変換する
  2. CSP(コンテンツセキュリティポリシー) — スクリプトの出所を制限し、許可していないスクリプトの実行をブラウザ側で止める
  3. HttpOnly Cookie — JavaScript から Cookie を読めなくし、万一スクリプトが動いても盗ませない

ポイントは「入力の混入ではなく、出力の扱いで防ぐ」こと。データは受け取ってよく、出す瞬間に無害化します。

登場人物メモ:

  • ※1 出力文脈 — 値を出す先(HTML・属性・JS・URL)。文脈ごとに変換規則が違う
  • ※2 CSP — スクリプトの出所を制限し、想定外の実行を止める仕組み

CSPとTrusted Types

エスケープを人手で完璧に保つのは難しい。そこで破られた前提で実行自体を止めるのが CSP と Trusted Types です。

  • CSP(Content Security Policy) — 「どの出所のスクリプトを実行してよいか」をブラウザに宣言する
    • nonce方式 — 応答ごとに使い捨ての乱数を付け、その nonce を持つ <script> だけ実行を許す
    • hash方式 — 許可するインラインスクリプトのハッシュを列挙する
    • strict-dynamic — nonce付きで信頼したスクリプトが動的に読み込む子スクリプトも連鎖的に信頼し、ホスト名の許可リスト管理から解放する
  • よくある回避unsafe-inline の残置・過度に広い許可ホスト・信頼した出所に置かれた JSONP や古いライブラリの悪用。CSPは補助であって、エスケープの代わりにはならない
  • Trusted TypesinnerHTML など危険なsinkに生の文字列を渡せなくするブラウザ機構。値は必ず「信頼済み型」を通す必要があり、サニタイズの通し忘れを型で強制できる

CSPが「実行の出所」を絞り、Trusted Typesが「sinkへの入力」を絞る。エスケープ(出口の変換)と合わせて三重に構えるのが上級の守りです。

なぜ入力フィルタでは防ぎきれないか

「危ない語を入口で消せばよい」が効かない理由を、もう一段はっきりさせます。

  • 文脈が入口では未定 — 同じ値がHTML本文にも属性にもURLにも出うる。入口では「どの脱出を防げばよいか」が決まらない
  • 抜け道が無限 — 大文字小文字・エンコード・全角/半角・コメント挿入・mXSSなど、ブラックリストは必ず抜かれる
  • 正当なデータを壊す<, &, 引用符は普通の文章にも現れる。入口で削ると機能が壊れる

だから入力検証は土台(想定外を弾く入口)に留め、XSSの主対策は出口の文脈別エスケープに置き、CSP/Trusted Types/HttpOnly Cookie で破られたときの被害を抑える。役割分担が答えです。

やさしく言うと(中級

入力検証は土台ですが、XSS を単独では防げません。検証を通った値でも、出力の文脈しだいで危険になるからです。

  • 入力検証 — 想定外を弾く(入口)
  • 出力エスケープ — 出す先で無害化する(出口・XSSの主対策)
  • CSP/HttpOnly — 破られたときの被害を抑える(多層防御)

⚠️ 上級の落とし穴

  • DOM型の見落とし — サーバ側だけ守り、innerHTML などブラウザ内のsinkを放置する
  • サニタイズ済みの再加工 — 一度浄化した文字列を後段で連結・再パースし、mXSSを招く
  • CSPを免罪符にするunsafe-inline を残す、広すぎるホスト許可、JSONP経由のバイパス
  • 文脈の取り違え — HTML用エスケープをJSやURLに流用し、属性やスキームで抜かれる
  • テンプレートの自動エスケープ過信innerHTML 相当のヘルパや「安全と印を付ける」機能で自動エスケープを外すと穴になる
やさしく言うと(中級
  • 入力検証だけに頼る — 保存された値が別の画面で出るときに漏れる(格納型XSS)
  • エスケープの文脈ミス — HTML用のエスケープを URL や JS にそのまま使う
  • innerHTML の乱用 — 受け取った文字列をそのまま HTML として挿入しない
  • CSPを免罪符にする — CSP は補助。エスケープの代わりにはならない

理解度チェック

そのまま解けます(成績は保存されません)。無料アカウントを作ると、学習の記録と進捗の山登りが始まります。

1. 出力先の文脈(HTML本文・属性・JS・URL・CSS)ごとにエスケープ処理を変える理由は?

2. DOM型XSSが「サーバ側の出力エスケープ」だけでは防ぎきれない最大の理由は?

3. DOM型XSSで、値が「コードやHTMLとして解釈されうる出口」を何と呼ぶ?

4. CSPのnonce方式の説明として正しいのはどれ?

5. mutation XSS(mXSS)が示す教訓として正しいのはどれ?

6. innerHTMLなど危険なsinkに「生の文字列を渡せなくし」、サニタイズの通し忘れを型で強制するブラウザ機構はどれ?

7. DOM型XSSで、攻撃者が値を注ぎ込める入口(location.hash や document.referrer など)を何と呼ぶ(英字)?

8. 自由なHTMLを許すとき、自作エスケープでなく許可リスト方式で浄化する定番のサニタイザ・ライブラリの名前は?