TL

DOM-based XSSのsink/source分類と緩和原理

クライアント側だけで起きるXSSを、データの流れから根絶できる。sourceからsinkへ抜ける経路を分類し、反射/格納型との違い、エスケープとTrusted Typesの効きどころを原理から押さえます。

応用セキュリティWebXSSDOMTrusted Types最終更新: 2026-06-21
TL;DR要点だけ先に
  • 1.DOM-based XSSはサーバーを介さず、ブラウザ内でsource(攻撃者が操れる入力)からsink(コードやマークアップを生む危険なAPI)へデータが流れて成立する。location.hashなどフラグメントが代表的source。
  • 2.反射/格納型がサーバーの出力に混入するのに対し、DOM型はレスポンス本文に痕跡が出ず、ペイロードがサーバーに届かないこともある。だからWAFやサーバー側エスケープでは検知も防御もしにくい。
  • 3.緩和の核は危険なsinkを使わず安全なsinkへ置換すること。textContentやsetAttributeへ寄せ、避けられないsinkはコンテキスト別エスケープかDOMPurifyで浄化し、Trusted Typesでsinkを型レベルに封じる。

DOM-based XSS は、サーバーが返した HTML には脆弱性が無いのに、ページ上で動く JavaScript が攻撃者の制御下にあるデータを危険な API へ渡してしまうことで成立する XSS です。攻撃のすべてがブラウザ内で完結するため、サーバーログにもレスポンス本文にも痕跡が残りにくく、従来のサーバー側対策をすり抜けます。ここでは「source から sink へのデータフロー」というモデルでその原理と緩和を整理します。

source と sink の定義

DOM-based XSS は、taint analysis(汚染解析)と同じ枠組みで捉えると正確に理解できます。登場人物は3つです。

用語意味代表例
source攻撃者が値を操作できる入力点location.hash / location.search / document.referrer / window.name / postMessage の data
sink文字列をコード・マークアップ・URLとして解釈する危険なAPIinnerHTML / outerHTML / document.write / eval / setTimeout(文字列) / location 代入
sanitizersourceとsinkの間で値を無害化する処理textContentへの置換 / エスケープ / DOMPurify / Trusted Types

脆弱性は「source の値が、無害化されないまま sink に到達する経路(taint flow)が存在するとき」に限って生じます。逆に言えば、source 単体・sink 単体は危険ではありません。location.hash を読んでも textContent に入れるだけなら安全ですし、innerHTML も定数を入れる限り無害です。問題は両者が一本の流れでつながった瞬間に起きます。

// 典型的なDOM-based XSS。hash(source)が無害化されずinnerHTML(sink)へ届く
const id = location.hash.slice(1);          // source: #<img src=x onerror=alert(1)>
document.querySelector('#tab').innerHTML = id; // sink: 文字列をHTMLとして解釈

innerHTML は代入された文字列をパースして DOM 部分木を構築します。このとき <script> 要素は仕様上その場では実行されませんが、<img onerror=...><svg onload=...> のようなイベントハンドラ属性は解釈・登録され、画像読み込み失敗などをトリガに任意コードが走ります。<script> が動かないからといって innerHTML が安全になるわけではない、というのが要点です。

反射型・格納型との本質的な違い

XSS は注入されたペイロードがどこで HTML に混入するかで3分類されます。DOM 型だけ混入箇所がサーバーではなくクライアントである点が決定的に異なります。

分類ペイロードがHTMLになる場所サーバーに届くかレスポンス本文に出るか
反射型 (Reflected)サーバーが当該リクエストの値を即座に埋めて返す届く出る
格納型 (Stored)サーバーが保存した値を後続レスポンスに埋める届く(保存される)出る
DOM型 (DOM-based)クライアントのJSがsinkで動的に生成する届かないことがある出ない

最も実務的に効くのが「届くか」「出るか」の2列です。フラグメント識別子(URL の # 以降)はブラウザがサーバーへ送信しないため、location.hash を source とする攻撃ではペイロードがサーバーに一切到達しません。結果として、サーバー側のエスケープも、WAF のシグネチャ検査も、サーバーログ監視も無力になります。検知と防御の責務がまるごとクライアントへ移るのが DOM 型の本質です。

同一コードに見えても分類は混入箇所で決まる

反射型・格納型は「サーバーがテンプレートに値を差し込む箇所」のエスケープ漏れが原因です。DOM 型は「ブラウザ上の JS が sink へ値を渡す箇所」が原因で、サーバー出力は正しくても発生します。1つのページが反射型と DOM 型を同時に抱えることもあり、サーバー側テンプレートとクライアント側 JS の両方を独立に監査する必要があります。

sink のコンテキスト分類

緩和の方針は sink が値を「何として」解釈するかで決まります。コンテキストを取り違えたエスケープは無力です。

sinkコンテキスト代表API必要な無害化
HTMLパースinnerHTML / outerHTML / document.write / insertAdjacentHTMLHTMLサニタイズ(DOMPurify等)かtextContentへ退避
JS実行eval / new Function / setTimeout(文字列) / setInterval(文字列)文字列を渡さない。関数参照を渡す
URLナビゲーションlocation代入 / a.href / window.openjavascript: スキームを拒否、許可スキームのみ通す
属性値setAttribute('srcdoc'/'on*'/...)イベントハンドラ・srcdocは値で設定しない

URL コンテキストは見落とされがちです。a.href = userInputuserInputjavascript:alert(1) だと、クリック時にスクリプトとして評価されます。HTML エスケープは効かず、スキームの許可リスト判定(https:http: と相対パスのみ通す等)が要ります。JS コンテキストの setTimeout も、第1引数に文字列を渡せばその文字列が eval 相当で実行されるため、必ず関数参照を渡します。

緩和の原理 — 危険な sink を断つ

DOM-based XSS の根本対策は「source を消す」ことではなく「source の値が危険な sink へ生で届かないようにする」ことです。優先順位の高い順に押さえます。

// 1. 安全なsinkへ置換する(最優先)。textContentはHTMLを解釈しない
el.textContent = location.hash.slice(1);     // <img ...> はただの文字列になる

// 2. 構造が要るならDOM APIで組む。文字列連結でHTMLを作らない
const a = document.createElement('a');
a.textContent = label;
if (/^https?:/.test(url)) a.href = url;       // スキームを検証してから設定

// 3. HTMLを通したい場合はサニタイザで浄化してからsinkへ
el.innerHTML = DOMPurify.sanitize(richText);  // 危険な要素・属性を除去

第一選択は安全な sink への置換です。innerHTMLtextContent に変えるだけで、文字列は HTML としてパースされず、いかなるタグもイベント属性も無効化されます。表示にマークアップが要らないなら、これだけで当該経路は閉じます。ノード操作のコストや live/static の挙動は DOMツリーの内部表現とノード操作のコスト も参照してください。

リッチテキストなど HTML 自体を通す必要がある場合に限り、サニタイザ(DOMPurify 等)で許可リストベースに要素・属性を絞ってから sink へ渡します。手書きの正規表現で <script> を消すだけの「ブラックリスト」は、<img onerror> や属性内の改行、HTML パーサの寛容な誤り訂正によって容易にすり抜けられるため、原理的に不完全です。

Trusted Types — sink を型で封じる

エスケープやサニタイズは「呼び出しを忘れた1か所」で破綻します。Trusted Types は、危険な sink が生の文字列を受け付けないようにブラウザ側で強制することで、この「うっかり」をプラットフォームレベルで排除する仕組みです。

Content-Security-Policy: require-trusted-types-for 'script'; trusted-types myPolicy
// このポリシーを通った値だけがTrustedHTMLになる
const policy = trustedTypes.createPolicy('myPolicy', {
  createHTML: (s) => DOMPurify.sanitize(s),    // 浄化を一元化
});
el.innerHTML = policy.createHTML(richText);    // 生文字列を直接代入すると例外

require-trusted-types-for 'script' を CSP で宣言すると、innerHTML などの「injection sink」へ普通の文字列を代入した時点で TypeError になります。通せるのは TrustedHTMLTrustedScriptTrustedScriptURL という専用型のオブジェクトだけで、それらは登録したポリシー関数を経由しないと生成できません。結果として、アプリ中に散らばった無数の sink 呼び出しを、ポリシー定義という一点の監査対象へ集約できます。サニタイズ漏れは「忘れ得るコード」から「定義しないと動かない構造」へ変わるわけです。

Trusted Types は CSP の上に乗る緩和層

Trusted Types は CSP のディレクティブとして配信され、nonce ベースの strict CSP と相補的に働きます。CSP がスクリプトの実行を許可リストで縛るのに対し、Trusted Types は危険な sink への到達を型で縛ります。両者の評価原理は Content Security Policyの内部動作と回避耐性 を参照してください。導入は Report-Only モードから始め、違反レポートで既存の sink 利用箇所を洗い出してからポリシー化するのが定石です。

postMessage と信頼境界

postMessage の受信ハンドラは見落とされやすい source です。受信した event.data を検証せず sink へ渡すと、別オリジンの窓から任意のペイロードを送り込まれます。対策は受信側で必ず event.origin を許可リストと照合し、データ構造も検証することです。

window.addEventListener('message', (event) => {
  if (event.origin !== 'https://trusted.example') return;  // origin検証は必須
  if (typeof event.data?.html !== 'string') return;        // 構造も検証
  el.textContent = event.data.html;                        // 安全なsinkへ
});

event.origin はブラウザが設定する改竄不能な値で、送信元の本当のオリジンを示します。ここを検査しないと、オリジンをまたぐ信頼境界が崩れます。オリジンとサイトの違い、窓どうしの信頼境界の原理は 同一オリジンポリシーとサイト分離の信頼境界 で整理しています。

DOM-based XSSはセッション奪取に直結する

任意の JS が走れば、攻撃者は同一オリジンの document.cookieHttpOnly 無しの場合)や localStorage 上のトークンを読み出して外部へ送れます。だから DOM 型対策は、Cookie とセッションHttpOnlySameSite重ねて初めて意味を持ちます。sink を断つクライアント側対策と、奪われても被害を抑えるトークン保護は、どちらが欠けても穴になります。

Web/フロントエンド Article

DOM-based XSSのsink/source分類と緩和原理を実務で読む

TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。

解決すること

セキュリティ

比較で見る軸

難易度: advanced / カテゴリ: Web/フロントエンド / タグ数: 5

導入後に効く点

反射/格納型がサーバーの出力に混入するのに対し、DOM型はレスポンス本文に痕跡が出ず、ペイロードがサーバーに届かないこともある。だからWAFやサーバー側エスケープでは検知も防御もしにくい。

先に潰すリスク

用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。

数字・仕様の読み方
難易度
advanced
カテゴリ
Web/フロントエンド
タグ数
5

判断チェックリスト

  • 自社の用途が「セキュリティ / Web」に近いか確認する。
  • 強みである「DOM-based XSSはサーバーを介さず、ブラウザ内でsource(攻撃者が操れる入力)からsink(コードやマークアップを生む危険なAPI)へデータが流れて成立する。location.hashなどフラグメントが代表的source。」が本当に評価軸になるか確認する。
  • 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
  • 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
  • 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
  • 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。

次に確認する観点

セキュリティWebXSSDOMTrusted TypesセキュリティWebXSS
参考: 公式情報