トンネルのエッジでのパス正規化: なぜ localhost ではルートがマッチするのにトンネルを経由するとマッチしないのか
localhost ではルートがマッチする理由と Cloudflare Tunnel を介した場合の不一致。cloudflared のパス正規化が %2F、ドットセグメント、スラッシュ、ingress ルールをどう変えるかを確認します。

Quick answer
cloudflared のパス正規化: なぜトンネルでルートが壊れるのか: localhost tunnel answer
A localhost tunnel gives your local app a public HTTPS URL without opening router ports, which is useful for demos, QA, mobile testing, and provider callbacks.
How do I expose localhost without opening ports?
Use a reverse HTTPS tunnel. Your machine connects outbound to the tunnel service, and the public URL forwards requests back to your local app.
When should I use a localhost tunnel?
Use one for webhook testing, OAuth callbacks, client demos, QA previews, mobile device checks, and short-lived development reviews.
Your /admin route works on localhost:8080. Through the tunnel, //admin returns something different, /public/../admin behaves differently again, and a rule you assumed was guarding the path never seems to fire. Nothing is wrong with your app. Several components are each parsing the same path with different rules, and a tunnel adds one more parser to the chain.
That chain is about to change. Starting 2027-01-25, new cloudflared releases will turn on URL normalization by default, which Cloudflare says may change which ingress rule matches a request. This guide explains what changes, what doesn’t, how path-confusion auth bypasses work, and how to test your own routing before the default flips.
要約
cloudflared2026.10.0 以降は、Ingress ルールにマッチする前に URL パスを正規化できます。- 現在はデフォルトでオフですが、2027-01-25 以降の新しいリリースではデフォルトでオンになります。
- Cloudflare はこの変更を、Access 検証を必要とするトンネルのルールに限定して適用すると述べており、他の設定には影響しません。
- 文書化された変換はドットセグメントの削除、バックスラッシュをスラッシュへ変換、連続するスラッシュの結合です。オリジンへ転送されるパスは変更されません。
- パーセントエンコード(例:
%2F)や末尾のスラッシュは文書化リストに含まれていないため、テストして確認してください。
Cloudflare が発表した内容
2026年10月22日、Cloudflare は「Cloudflare Tunnel supports URL normalization.」というタイトルのチェンジログエントリを公開しました。主な事実は次のとおりです:
- 入手可能性:
cloudflaredバージョン2026.10.0以降。GitHub には 2026.10.0 が 2026年10月5日にリリースと記載されています。2026年11月5日時点で私が確認できる最新リリースは 2026.10.6。 - 現時点のデフォルト: オフ。フラグは反転されており、文書化されたデフォルトの
--disable-path-normalizationはtrue。 - 2027-01-25 以降のデフォルト: オン。新しいリリースで適用。
- 適用範囲: Cloudflare は、Access 検証を要求するルールを持つトンネルのセキュリティを向上させると述べ、そうしたルールがない設定は影響を受けないとしています。
- Origin path: 変更なし。正規化は マッチング に使用されるパスに適用され、転送されるパスには適用されません。
今すぐ有効にするには:
cloudflared tunnel --disable-path-normalization=false run 3cTUNNEL_NAME_OR_UUID3e
または各 cloudflared レプリカ上で TUNNEL_DISABLE_PATH_NORMALIZATION=false を設定して再起動します。
運用上の注意点として、スタンドアロンの cloudflared バイナリはサービスとして実行される場合、デフォルトで毎 24 時間 auto-update します(autoupdate-freq)。パッケージマネージャによるインストール、Windows 実行、対話型実行は自動更新されません。トンネルが自動更新される場合や latest のイメージタグを採用すると、新しいデフォルトは意図的なアップグレードなしに到来します。今すぐフラグを明示的に設定するか、テスト完了までバージョンを固定してください。
何が正規化され、何がされないか
Cloudflare の設定ドキュメントは、挙動を 3 つの例で説明しています。受信パスは cloudflared が受け取る形のまま示されます:
| 受信パス | ingress マッチに使用 | オリジンへ送信 |
|---|---|---|
/hello/../path |
/path |
/hello/../path |
/hello//path |
/hello/path |
/hello//path |
/hello\path |
/hello/path |
/hello\path |
言葉で言えば、ドットセグメント (. および ..) は RFC 3986 5.2.4 に従い削除され、バックスラッシュはスラッシュへ変換され、連続するスラッシュは結合されます。文書には *パーセントデコード*、末尾スラッシュの処理、セミコロン・パスパラメータ、ケースフォールディングは含まれていません。RFC 3986 はここで厳密な線を引くため、正規化処理がどこまで及ぶかを理解することが重要です。パーセントエンコードされた unreserved 文字はリテラルと同等とみなされ、正規化処理でデコードされることがあります。一方、reserved 文字は等価ではなく、%2F は / と同一には扱われません。標準準拄の正規化器はそれらを潰してしまうべきではありません。
私の結論は、Cloudflare の3つの文書化された変換以外のものはすべて「正規化されていない」とみなすべきだということです。自分のバージョンでの挙動を確認するには、以下のテストマトリクスを実行して確認してください。Cloudflare の公式ドキュメントを基に検証しましたが、cloudflared を実際に計測したわけではないため、下のマトリクスが挙動を確かめる方法です。
なぜ localhost とトンネルは異なるのか
localhost:8080/admin にヒットすると、少なくとも 3 つのパスパーサーが関与します:
- Cloudflare のエッジ, 自身の URL 正規化機能を備えています。Cloudflare のドキュメントによれば、受信 URL の正規化はデフォルトで有効で、基礎的な手順(隣接するスラッシュの結合 など)は常に適用され、無効化できません。
cloudflared, パスをあなたの ingress ルールに対してマッチさせます。- あなたのオリジン, そのフレームワーク、リバースプロキシ、または静的ファイルハンドラが独自のデコードとドットセグメントのロジックを適用します。
第四の、見落とされがちな参加者は、テストクライアントです。ブラウザは要求を送信する前に .. セグメントを解決します。また WHATWG の URL パーサは http/https URL においてバックスラッシュをスラッシュとして扱います。したがって、ブラウザでクリックしてもこの入力をほとんど試すことはありません。たとえ curl を使っても、dot セグメントを畳んでしまうことがあります。 などの入力を送信しないと、実際の挙動を検証できません。
パーサー間の不一致は回避の入口になり得ます。レイヤー2 がリクエストを /public/... 用として判断し、レイヤー3 が /admin 用として判断している場合、1つのルールがもう1つと矛盾して評価されることになります。
パス混乱認証バイパスの手順
cloudflared が Access 検証で /admin を保護するトンネルを考え、Access ポリシーの背後に防御を二重化します:
tunnel: <TUNNEL_UUID>
credentials-file: /etc/cloudflared/<TUNNEL_UUID>.json
ingress:
- hostname: app.example.com
path: ^/admin(/|$)
service: http://localhost:8080
originRequest:
access:
required: true
teamName: <your-team-name>
audTag:
- <access-application-aud-tag>
- hostname: app.example.com
service: http://localhost:8080
- service: http_status:404
Ingress ルールは先頭から末尾へ評価され、path は Go の regexp/syntax パッケージで解析される正規表現です。では GET /public/../admin/users を送ってみてください。
- 正規化オフ: マッチャーは
/public/../admin/usersを見ます。最初のルールの正規表現は/adminで始まるパスを要求するため、マッチしません。リクエストは 2 番目のルールへとフォールスルーされ、Access 要件がなく、元のパスのまま転送されます。 - Your origin はドットセグメントを畳み込み、
/admin/usersへリクエストを振り、応答します。/adminを保護するはずだった JWT チェックは実行されません。 - 正規化オン: マッチャーは
/admin/usersを見て、最初のルールを選択し、有効な Access JWT を要求します。オリジンは元のパスをまだ受け取ります。
このパターンは一般的です。パスの1つの解釈に基づいてセキュリティ決定がなされ、別の解釈に基づいてアクションが実行される、というものです。これは、Orange Tsai の 2018 Black Hat および DEF CON の研究「Breaking Parser Logic」が指摘したことです。彼のスライドは、/foo;name=orange/bar/ が Apache、Nginx、IIS では /foo;name=orange/bar/ と読まれますが、Tomcat と Jetty では /foo/bar/ と読まれ、それを利用して proxy レベルの ACL を回避できることを示しました。
この現象は今年も続いています:
- Heimdall (CVE-2026-42274): バージョン 0.17.14 未満で、アイデンティティ認証対応プロキシが未正規化パスでルールをマッチさせ、下流のコンポーネントがドットセグメントを正規化していたため、
/user/../adminのようなリクエストが 1 つのパスとして許可され、別のパスとして処理される可能性がありました。 - Quarkus (CVE-2026-50559): パスベースの認可ポリシーは、パーセントエンコードされたセミコロン(
%3B)、スラッシュ(%2F)またはバックスラッシュ(%5C)を用いた場合にバイパスされる可能性がありました。セキュリティレイヤとルーティングや静的ファイルレイヤがそれらを異なる方法でデコードしたためです。 - @opennextjs/cloudflare (CVE-2026-3125):
/cdn-cgi\\image/のバックスラッシュが Cloudflare のエッジ検知をすり抜け、その後 Worker’s URL クラスがそれを前方スラッシュに変換し、脆弱なハンドラへ到達しました。修正は 1.17.1 で行われました。 - fast-uri (CVE-2026-6321): ライブラリはドットセグメントを削除する前に、パーセントエンコードされた区切り文字とドットセグメントをデコードしてしまうため、異なる URI が 1 つの正規化されたパスに畳み込まれることがありました。3.1.1 で修正。
スタックが異なっても根本原因は同じです。2 つのパーサー、1 つのパス。
テストする4つの入力
ドットセグメント (/../ および /./)
これは classic な traversal-style の混乱で、new cloudflared の設定が直接対処する点です。 plain .. およびエンコード形式 %2e%2e、%2E%2E をテストします。RFC 3986 はパーセントエンコードされた未予約文字をリテラルと同等とし、WHATWG の URL パーサは %2e をパス・セグメントのドットとして扱います。多くのオリジンは %2e をドットを削除する前にデコードします。Cloudflare はドット・セグメント削除を文書化していますが、エンコードされたドットが先にデコードされるかは明記していません。その点を検証するには現場でのテストが必要です。
ダブルスラッシュ (//)
フレームワークごとに、//admin が /admin と等価かどうかの解釈が異なります。いくつかはそれをマージしますが、空のセグメントを意味のあるものと扱うものもあり、またいくつかはそれを拒否します。Cloudflare の cloudflared 正規化は、マッチングのみのためにそれらを結合します。^/admin のようなパターンは正規化なしでは //admin にマッチしません。正規化を行うオリジンは //admin を /admin として提供することもあります。
末尾のスラッシュ (/admin vs /admin/)
文書化された正規化は末尾のスラッシュには触れていません。したがってそれらは変更されないと見なします。これにより正規表現のアンカーが決定的になります。^/admin$ は /admin にのみマッチし、/admin/ にはマッチしません。一方、多くのフレームワーク(例えば Express はデフォルトで)両方を同じハンドラにルーティングします。保護対象のパスを ^/admin(/|$) のように書くか、あるいはホスト全体を保護して公開例外を設けると良いでしょう。
エンコードされたスラッシュ (%2F) およびその仲間
%2F はパーサーが分割する地点です。いくつかの起点はルーティング前に実際のセパレータへデコードしますが、他は単一セグメント内に保持します。RFC 3986 は / と等価ではないと見なすため、cloudflared の正規化にはこれをデコードする明確な理由がありません。標準準拠の正規化では /admin%2Fusers を 1 つの不透明なセグメントとして扱う可能性があります。オリジンがそうしない場合もあります。 %5C(バックスラッシュ)や %3B(セミコロン)も同様です。Quarkus の事例は、回避がちょうどこのギャップに潜むことを示しています。
今日実行できるテストマトリクス
上記の設定を前提に、オリジンが /admin/* に対してのみ 200 を返し、Access JWT が存在する場合のみとします。各リクエストに対するパスの通過条件は、プレーンな /admin/users と同様に拒否されることです。
| リクエストパス | オフ時のマッチ | オン時のマッチ | 文書化済み? |
|---|---|---|---|
/admin/users |
/admin/users |
/admin/users |
基準 |
/public/../admin/users |
raw | /admin/users |
Yes |
//admin/users |
raw | /admin/users |
Yes |
/admin//users |
raw | /admin/users |
Yes |
\admin\users |
raw | /admin/users |
Yes |
/admin/ |
raw | raw | Trailing slash not listed |
/%61dmin/users |
raw | Not documented | No, test it |
/admin%2Fusers |
raw | Not documented | No, test it |
/public/%2e%2e/admin/users |
raw | Not documented | No, test it |
/admin;x=1/users |
raw | Not documented | No, test it |
結果を読む際の二つの注意点。
“ first, Cloudflare の自体のエッジ正規化は別レイヤーです。エッジがすでに書き換えを行っている場合、cloudflared 正規化の有無に関わらず同じように見えることがあります。大丈夫ですが、どのレイヤーが実際の処理を担当しているかを把握したいです。”
“ second, cloudflared tunnel ingress rule <URL> はどのルールにマッチするかを示しますが、Cloudflare のドキュメントは正規化フラグを尊重するかどうかを明示していません。実トラフィックで確認してください。”
Here’s a small Python harness that sends paths exactly as written, which curl and browsers won’t always do:
import http.client, ssl
HOST = "app.example.com"
PATHS = [
"/admin/users", "/public/../admin/users", "//admin/users",
"/admin//users", "\\admin\\users", "/admin/", "/%61dmin/users",
"/admin%2Fusers", "/public/%2e%2e/admin/users", "/admin;x=1/users",
]
ctx = ssl.create_default_context()
for path in PATHS:
conn = http.client.HTTPSConnection(HOST, context=ctx, timeout=10)
conn.request("GET", path)
print(f"{path!r:34} -\u003e {conn.getresponse().status}")
conn.close()
Run it twice: once with the tunnel on current defaults, once with --disable-path-normalization=false. Any path that returns something other than the baseline denial is a finding. For one-off checks, curl --path-as-is stops curl from collapsing dot segments before sending.
Rollout checklist before 2027-01-25
- 対象範囲を把握。
access:ブロックにrequired: trueが含まれるか configs を検索してください。該当するルールがなければ Cloudflare は影響を受けないとしています。 accessブロックの配置を確認。 Cloudflare のドキュメントリポジトリの公開済みの issue では、cloudflared2026.7.1 でトップレベルのoriginRequest.accessブロックが適用されず、個別 ingress ルールの下のブロックは適用されたと報告されています。これはユーザーレポートであり、公式ドキュメントではないため、あなたのバージョンで検証してください。- 正規化オフ→オンのマトリクスを staging で実行し、差分を記録。
- 正規化のアンカーを監査。 保護対象のパスに対して、末尾スラッシュを処理するパターンへ置換します。
- 可能な限り早く、明示的にオンへ。 今すぐフラグを設定すると、1月の変更はあなたにとってノーオペレーションです。移行後はリリースノートでフラグの挙動を確認してください。
- 自動更新の制御(
--no-autoupdate、バージョン固定)を行い、マトリクスが新バージョンで通過するまで待ちます。 - オリジン側の対応も。 トンネルの正規化は防御の深層であり、アプリケーションが曖昧なパス(エンコード済みスラッシュ、バックスラッシュ、ドットセグメント)を拒否するようにしてください。
- 生データのリクエストターゲットを記録。 探索試行がログに現れた場合、
%2e%2eや\が表示され、きれいなパスとして表示されないようにします。
よくある質問
この変更は、私のアプリケーションが受け取るパスを変えますか? いいえ。Cloudflare はオリジンへ送られる URL パスは同じままだとしています。正規化されるのは ingress ルールを選択するために使用されるパスだけです。
2027-01-25 に私のトンネルは壊れますか? アクセス検証を要求するルールがあり、未正規化のパスに依存してマッチしている場合に限ります。Cloudflare はこのようなルールを持たない構成には影響がないとしていますが、日付前にパスベースのルーティングをテストすることを推奨しています。
トンネルで正規化すれば、パス混乱のバイパスを止められますか?
それが実行される変換のギャップを埋めますが、オリジンの解析とトンネルの解析を完全に一貫させるわけではなく、%2F のような予約文字のデコードを Cloudflare が文書化していない点に注意してください。これを1つのレイヤーとして扱ってください。
他のトンネルやリバースプロキシでも同じリスクがありますか? はい。パスに対するルーティングや認可の決定を、次のレイヤーと同じ方法で正規化しないコンポーネントは、露出します。エッジで動作するマトリクスを適用してください。
Webhook の署名検証はトンネルの背後で別の故障モードであり、署名に関する前の投稿で扱われています。
出典
- Cloudflare Tunnel supports URL normalization (changelog, Oct 22, 2026)
- Cloudflare Tunnel configuration file: how traffic is matched
- Cloudflare Tunnel run parameters: disable-path-normalization
- Cloudflare Tunnel origin parameters: Access settings
- Cloudflare URL normalization (Rules docs)
- cloudflared releases on GitHub
- cloudflare-docs issue 32006: access block placement
- RFC 3986: Uniform Resource Identifier (URI): Generic Syntax
- Orange Tsai, “Breaking Parser Logic” (Black Hat USA 2018 slides)
- CVE-2026-42274 (Heimdall)
- CVE-2026-50559 (Quarkus) advisory
- CVE-2026-3125 (@opennextjs/cloudflare)
- CVE-2026-6321 (fast-uri)
Related InstaTunnel pages
Continue from this article into the most relevant product guides and workflows.
Related Topics
Keep building with InstaTunnel
Read the docs for implementation details or compare plans before you ship.