以下の内容はhttps://cysec148.hatenablog.com/entry/2025/08/21/172203より取得しました。


【有料試作版】PortSwigger LAB解説:続・DOM XSS in document.write sink using source location.search(document.write × location.search でDOM XSS)

Hello there, ('ω')ノ

DOMで追加された要素はなぜ「ソース」に無いのに「Elements」では見えるのか

document.write と DevTools で読み解く表示の流れ

この記事では、次の2点をセットで解説します。
- 要素(Elementsパネル)にあるのに、ページのソースには無い理由
- document.write を使ったページの表示の流れ(サーバ + ブラウザ)


なぜ「ソース」には無くて「Elements」にはあるのか

  • ページのソース表示(View Source) は、サーバから受け取った 初期HTML(生のレスポンス) をそのまま見せます。
    JavaScript があとから DOM を変更しても ソース表示には反映されません
  • Elements(要素)パネル は、JavaScript 実行後の “現在のライブDOM” を表示します。
    document.write / innerHTML / appendChild などで追加・変更されたノードも すべて見えます

今回のケースでは、インラインスクリプトがパース中に実行されて以下を差し込みます:

document.write('<img src="/resources/images/tracker.gif?searchTerms=' + query + '">');

この <img>初期HTMLには存在しない ため、ソースには出ず、Elementsには出る――これが答えです。

ライブDOMをテキストで確認したいときは Console で document.documentElement.outerHTML を見ると、挿入後のHTML全体が取得できます。


表示までの流れ(サーバ + ブラウザ)

1) ユーザの操作

  • 画面の検索フォーム <form action="/" method="GET"> にキーワード(例: test)を入力して送信。
  • ブラウザが GET /?search=test をサーバへ送る。

2) サーバ側(サーバサイドレンダリング)

  • サーバはクエリ search=test を受け取り、ブログ記事を検索。
  • ヒット件数(ここでは 0)と検索語('test')をテンプレートに埋め込んで HTML を生成。

    例:

  <h1>0 search results for 'test'</h1>
  • 生成済みの HTML をレスポンスとして返す。

    ※ この「0 search results for 'test'」はクライアント側JSではなく サーバ側 で組み立てられて返ってきている。

3) ブラウザのHTMLパースと描画

  • 受け取ったHTMLを順にパースして描画。
  • <section class="blog-header"> 内の
  <h1>0 search results for 'test'</h1>

がそのまま画面に表示される。

4) クライアント側JavaScriptの実行(トラッキング用)

  • インラインスクリプトが実行される:
  function trackSearch(query) {
    document.write('<img src="/resources/images/tracker.gif?searchTerms='+query+'">');
  }
  var query = (new URLSearchParams(window.location.search)).get('search');
  if (query) {
    trackSearch(query);
  }
  • window.location.search(例: ?search=test)を URLSearchParams で解析し、query = "test" を取得。
  • trackSearch("test") が呼ばれ、document.write により以下が スクリプトの位置 に挿入される:
  <img src="/resources/images/tracker.gif?searchTerms=test">
  • その結果、/resources/images/tracker.gif?searchTerms=test へのリクエストが送られ、トラッキングが行われる。 ※ document.writeパース中 に実行されると、その位置に書き込みを行う(後からDOMに append するのではなく、文書ストリームへ書くイメージ)。

5) 結果画面

  • 上部(ブログヘッダ)にはサーバが埋め込んだ
  <h1>0 search results for 'test'</h1>

が表示。

  • 検索結果リストは 0 件なので、<section class="blog-list no-results"> 内の「Back to Blog」リンクのみが見える。

  • 同時に、トラッキング用 <img> も読み込まれている。


Elementsパネルで実際の <img> を確認する手順

すぐに該当ノードへジャンプ(最短)

Console に以下を入力し Enter:

const el = document.querySelector('img[src*="tracker.gif"]');
inspect(el);  // Elementsパネルでそのノードにフォーカス

Elements内検索

  • Elementsをアクティブにして Ctrl + F(Mac: Cmd + F
  • 検索語:tracker.gif / img[src*="tracker.gif"] / searchTerms=

挿入の瞬間を捕まえる(DOMブレークポイント)

  1. Elements で <section class="search"><body> を右クリック
  2. Break on → Subtree modifications
  3. リロードすると、挿入時に Sources で停止し、Call Stack から発火元へ辿れる

位置の目安:今回のHTML構造では <section class="search"><section class="blog-list"> の間(インライン <script> の直後)に <img> が現れます。


実装・調査のTIPS

document.write 実行時に必ず止める(発火元特定)

// Consoleで実行 → リロードすると document.write 実行時に停止
debug(Document.prototype.write);   // または debug(document.write)

// 調査終了後に解除
undebug(Document.prototype.write);

ライブDOMをテキストで確認

document.documentElement.outerHTML

セキュリティのポイント(DOM XSS の危険)

  • 0 search results for 'test'」の生成は サーバ側ロジック。JS は件数や見出しの生成に関与していません。
  • JS は location.search から検索語を取り出し、document.write でトラッキング用 <img> を差し込むだけ。
  • 未エスケープの query を文字列連結して document.write に流し込むのは危険です。 悪意ある入力で <script> などを注入され、DOM XSS に繋がります(本ラボの主題)。

まとめ

  • ソース表示 = 初期HTML(静的)Elements = 実行後DOM(動的)document.write などで追加された要素はソースには無いが Elements には見える。
  • 表示の流れは「ユーザ入力 → サーバで件数表示 → クライアントJSが document.write<img> を挿入」。
  • 調査は Elements検索/inspect()DOMブレークポイントdebug(Document.prototype.write) が即効。
  • セキュリティ的には、入力の適切なエスケープと、document.write の利用回避を検討する(createElement + setAttribute など)。

Best reagards, (^^ゞ




以上の内容はhttps://cysec148.hatenablog.com/entry/2025/08/21/172203より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14