Dev Study
← Webセキュリティの実務
レッスン 7 / 12·約 9 分·第19期 Webセキュリティの実務

サーバに届かない経路 — フラグメントとシンク

リクエストがアプリケーションに届くまで
  1. ブラウザURL入力今ここ
  2. DNS名前→IP
  3. ルータ経路選択
  4. ロードバランサ振り分け
  5. Nginx受け口
  6. アプリ処理
  7. DB保存

前回は、ページの内側のスクリプトがそのオリジンとして動くことを見ました。今回は、要求に載らず、サーバ側のどの仕組みにも現れない経路を扱います。

まず結論

URL の `#` 以降(フラグメント)は仕様上リクエストに載りません。サーバのログにも、要求を検査する WAF にも現れませんが、ページ内のスクリプトからは読めます。

このコースの芯は「ブラウザが確かめられるのは『どこから来たか』まで、『本人が望んだか』はサーバが自分で確かめるしかない」でした。ところがサーバは、届かない値を確かめられません。この経路では、確かめる場所を値が使われる場所(シンク)へ移すしかありません。

? 読む前に予測

自前サーバ(127.0.0.1)のページを、headless の Chrome for Testing 151 で `http://127.0.0.1:18707/page?q=1#section-2` として開きました。サーバに届いたものの説明として正しいのはどれでしょうか。

当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。

抽象原理

URL の # 以降は、ブラウザの手元で切り離される

このコースでは、「本人が望んだか」はサーバが自分で確かめるしかない、と見てきました。ただしサーバが確かめられるのは、要求に載って届いたものだけです。URL の中に、載らない部分があります。

RFC 3986 §3.5 はフラグメントを # から URI の末尾までと定め、参照を解決する前に切り離すと書きます。

the fragment identifier is separated from the rest of the URI prior to a dereference, and thus the identifying information within the fragment itself is dereferenced solely by the user agent, regardless of the URI scheme.

RFC 9110 §7.1 は HTTP の側から言い直します。

The target URI excludes the reference's fragment component, if any, since fragment identifiers are reserved for client-side processing ([URI], Section 3.5).

「サーバーには送信されない」は既存クイズの結論と同じです。本回が使うのは、§17.11 の冒頭の後半です。

Although fragment identifiers used within URI references are not sent in requests, implementers ought to be aware that they will be visible to the user agent and any extensions or scripts running as a result of the response.

サーバには届かないが、応答の結果として動くスクリプトには見える。 値を観測できる場所と、値が使われる場所が分かれています。

フラグメントが届く先と、届かない先
URL/page?q=1#section-2
ブラウザが # で切り分ける
要求に載る部分/page?q=1
手元に残る部分#section-2
それぞれが届く先
サーバ・ログ・WAF# 以降を見る機会が無い
ページ内のスクリプトlocation.hash で読める
値が使われる場所
シンク文字列を解釈する API。止められるのはここ
  • ・RFC 9110 §7.1: 対象 URI はフラグメントを含まない
  • ・§17.11: 要求には載らないが、応答の結果動くスクリプトと拡張機能には見える
具体実物

自前サーバのソケットに届いた1行目

127.0.0.1 だけで待ち受ける自前の node サーバを立て、ソケットに届いた生のバイトから1行目をそのまま記録しました。ページは location.hash を textContent で表示するだけで、シンクは使っていません。Chrome for Testing 151(headless、Playwright 経由)で2つの URL を開きました。

run1 の抜粋(2026-09-24、macOS。行を省略、順序は原文のまま)コマンド出力
[browser] final page.url(): http://127.0.0.1:18707/page?q=1#section-2
[browser] #out: location.hash=#section-2 location.href=http://127.0.0.1:18707/page?q=1#section-2
[browser] final page.url(): http://127.0.0.1:18707/landing#keep-me
--- server log
[server] raw request line: "GET /page?q=1 HTTP/1.1"
[server] raw request line: "GET /favicon.ico HTTP/1.1"
[server] raw request line: "GET /r HTTP/1.1"
[server] raw request line: "GET /landing HTTP/1.1"

ページ内のスクリプトは location.hash=#section-2 を読めています。サーバに届いた行は GET /page?q=1 HTTP/1.1 です。3回とも同じで、curl で送っても同じ行でした。/favicon.ico はブラウザが自動で取りに行ったもので、本題とは関係ありません。

2つ目の URL /r#keep-me に、サーバはフラグメントの無い Location: /landing で 302 を返します。最終 URL は /landing#keep-me でした。§10.2.2 は、Location にフラグメントが無ければ元のフラグメントを引き継ぐよう求めています。サーバは #keep-me を一度も受け取らないまま、その値を次のページへ運ぶ経路になっています。 §17.11 が注意しているのは、これが別サイトへのリダイレクトだった場合です。

✓ ここまでの確認

サーバのアクセスログには `GET /page?q=1 HTTP/1.1` だけが残っていました。ここから言えることはどれですか。

抽象原理

値が危険になるのは、届いた場所ではなく使われる場所

OWASP の DOM based XSS Prevention Cheat Sheet は、XSS を値が埋め込まれる場所で分けます。

Reflected and Stored XSS are server side injection issues while DOM based XSS is a client (browser) side injection issue.

ブラウザの中で埋め込まれるなら、効くのはブラウザの中の対策です。Trusted Types の §2.1(非規範の解説節)は、その場所をシンクと呼びます。

An injection sink is a powerful Web API function that should only be called with trusted, validated or appropriately sanitized input.

続けて、シンクに渡る文字列は do not carry the information about their provenance(出自の情報を持たない)と書きます。フラグメントから来た文字列も自分で書いた定数も、シンクの前では同じ文字列です。

『1行のHTMLが画面に出るまで』の「JSが割り込む」回で、innerHTML は同じ HTML パーサを呼び、入れた <script> は評価されないことを見ました。それでも §2.1.1 は Element.innerHTML を HTML parsing sinks に数えています。script が走らないことと、シンクでないことは別です。

直す場所もシンクです。OWASP の RULE #7 は The best way to fix DOM based cross-site scripting is to use the right output method (sink). と書き、既存クイズも表示には textContent を選びます。Trusted Types は、危険なシンクに素の文字列を渡せなくする API です(lock down injection sinks to only accept non-spoofable, typed values in place of strings)。Chrome 83・Firefox 148・Safari 26.0 が対応を告知しています。

具体実物

要求を検査する WAF の対象に、# 以降の欄は無い

『Webを支えるソフトウェア』の「WAFは何を見ているのか」では、CRS v4.29.0 のルール942100 を追いました。検査対象はクッキーの値と名前、User-Agent、Referer、パラメータの名前と値、XML のノードと属性です。

並べると、どれも届いた要求の中の欄です。値が URL そのものである欄の Referer からも、§10.1.3 がフラグメントを除いています(MUST NOT include the fragment)。この一覧に # 以降が入る欄はありません。

「要求に無いものは検査できない」は、あのレッスンが述べたことではなく、検査対象の一覧と RFC 9110 から本回が導いた結論です。ルールが何を検出するかとは関係なく、観測点を通らない値は、どのルールの対象にもなりません。

✓ ここまでの確認

要求の中身を検査する WAF がある環境で、`location.hash` を読んでページ内で処理するコードをどう評価すべきですか。

⇄ 2つを見比べる
具体例 A

自前サーバが受け取った1行目

ブラウザの URL とページ内の location.hash には #section-2 がありますが、ソケットに届いた行には無い。ログの設定では変えられません。

#section-2 が見えるか(実測)表
見る場所 見えるか
ブラウザの URL 見える
location.hash 見える
サーバの1行目 見えない(GET /page?q=1)
具体例 B

WAF ルール942100 の検査対象

検査対象はどれも要求の中の欄です。値が URL そのものである Referer からもフラグメントは仕様で除かれるので、ルールの対象に入る経路がありません。

検査対象と # 以降表
検査対象 # 以降を含むか
クッキー・User-Agent 含まない
パラメータの名前と値 含まない
Referer 含まない(§10.1.3)

ログと WAF は目的が違います。両方でフラグメントが現れない理由として、共通しているものは何でしょうか。

どちらも「何を材料にしているか」を先に確かめてください。

この章のまとめ

  • フラグメントは参照の解決前に切り離され(RFC 3986 §3.5)、対象 URI に含まれない(RFC 9110 §7.1)。要求に載らない。
  • 実測でも、ソケットに届いた行は GET /page?q=1 HTTP/1.1。ページ内では location.hash=#section-2 が読めた。
  • §17.11: 送られないが、応答の結果動くスクリプトには見える。リダイレクトでは引き継がれる(§10.2.2)。
  • OWASP は DOM based XSS をクライアント側の注入と呼ぶ。直す場所はシンク。innerHTML は script が走らなくてもシンク。
  • WAF の検査対象は要求の欄だけ。Referer からもフラグメントは除かれる。「見えない場所の異常なし」は証拠にならない、は本回の導出。
  • Trusted Types は Chrome 83・Firefox 148・Safari 26.0 が対応を告知。文書は Working Draft。
  • 「絶対に漏れない」ではない。実装の不具合で漏れた例が RFC 9700 §4.2 にある。

自分の現場で確かめる

読んだだけでは身につきません。手元の環境で実際に見てみてください。

理解度チェック

読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。

問題 1 / 3正解: 0

OWASP の DOM based XSS Prevention Cheat Sheet は、反射型・格納型の XSS と DOM based XSS の違いをどう書いていますか。

用語集(4語)
フラグメント
URL の `#` から末尾まで。RFC 3986 §3.5 は参照の解決前に切り離すと定め、RFC 9110 §7.1 は対象 URI から除く。要求には載らないが、ページ内のスクリプトには見える。
シンク (injection sink)
渡された文字列を解釈・実行しうる API。Trusted Types §2.1(非規範の解説節)は、信頼できる入力だけで呼ぶべき強力な関数と説明する。`innerHTML` や `eval` など。
DOM based XSS
値がブラウザ内の実行時にページへ入り込む XSS。OWASP はクライアント(ブラウザ)側の注入の問題と呼び、サーバ側の注入である反射型・格納型と区別する。
Trusted Types
シンクに文字列ではなく、ポリシーから作った型付きの値だけを受け付けさせる API。W3C Working Draft(2026年6月23日)。