InstaTunnel
Basculer le menu de navigation
Tutorial
10 min read
19 views

Normalisation du chemin à l’extrémité du tunnel : pourquoi une route correspond sur localhost mais pas via un tunnel

Pourquoi les routes correspondent sur localhost mais pas via un Cloudflare Tunnel. Voyez comment la normalisation du chemin par cloudflared modifie %2F, les dot-segments, les barres obliques et les règles d’ingress.

IT
InstaTunnel Team
Published by the InstaTunnel team | Editorial policy
Normalisation du chemin à l’extrémité du tunnel : pourquoi une route correspond sur localhost mais pas via un tunnel

Quick answer

Normalisation du chemin avec cloudflared : pourquoi les itinéraires échouent sur les tunnels: 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.

Votre /admin route fonctionne sur localhost:8080. À travers le tunnel, //admin renvoie quelque chose de différent, /public/../admin se comporte différemment encore, et une règle que vous pensiez protéger le chemin ne semble jamais se déclencher. Rien ne cloche dans votre application. Plusieurs composants interprètent le même chemin selon des règles différentes, et un tunnel ajoute un parseur supplémentaire à la chaîne.

Cette chaîne est sur le point de changer. À partir du 2027-01-25, les nouvelles versions de cloudflared activeront la normalisation des URL par défaut, ce que Cloudflare dit pouvoir modifier la correspondance des règles d’ingress pour une requête. Ce guide explique ce qui change, ce qui ne change pas, comment les bypass d’authentification par confusion de chemin fonctionnent et comment tester votre propre routage avant le basculement par défaut.

Résumé rapide

  • cloudflared 2026.10.0 et versions ultérieures peuvent normaliser le chemin URL avant le matching des règles d’ingress.
  • Il est désactivé par défaut aujourd’hui et activé par défaut dans les nouvelles versions à partir du 25-01-2027.
  • Cloudflare précise que ce changement s’applique uniquement aux tunnels avec des règles d’ingress nécessitant une validation Access ; les configurations sans de telles règles ne sont pas affectées.
  • Les transformations documentées sont la suppression des dot-segments, la conversion backslash en slash et la fusion des slashes répétés. Le chemin transmis à votre origine n’est pas modifié.

Ce que Cloudflare a annoncé

Le 22 octobre 2026, Cloudflare a publié une entrée de journal des modifications intitulée « Cloudflare Tunnel prend en charge la normalisation des URL ». Les faits clés :

  • Disponibilité : cloudflared version 2026.10.0 et ultérieures. GitHub répertorie 2026.10.0 comme publiée le 5 octobre 2026 ; la version la plus récente que j’ai pu trouver au 5 novembre 2026 est la 2026.10.6.
  • Par défaut aujourd’hui : désactivé. Le drapeau est inversé, donc la valeur par défaut documentée de --disable-path-normalization est true.
  • Par défaut à partir du 2027-01-25 : activé, pour les nouvelles versions.
  • Portée : Cloudflare indique que cela améliore la sécurité pour les tunnels avec des règles qui nécessitent une validation Access, et que les configurations sans de telles règles ne sont pas affectées.
  • Chemin d’origine : inchangé. La normalisation s’applique au chemin utilisé pour le matching, et non au chemin transmis à l’origine.

Pour l’activer maintenant :

cloudflared tunnel --disable-path-normalization=false run <TUNNEL_NAME_OR_UUID>

ou définir TUNNEL_DISABLE_PATH_NORMALIZATION=false sur chaque réplique de cloudflared et le redémarrer.

Un détail opérationnel mérite attention. Les binaires autonomes cloudflared qui tournent comme service se mettent à jour automatiquement toutes les 24 heures par défaut (autoupdate-freq). Les installations par gestionnaire de paquets, Windows et les exécutions interactives ne se mettent pas à jour automatiquement. Si votre tunnel se met à jour automatiquement, ou si vous déployez une image tag latest flottante, la nouvelle valeur par défaut arrivera sans mise à niveau délibérée. Définissez soit explicitement le drapeau dès maintenant soit fixez votre version jusqu’à ce que vous ayez testé.

Ce qui se normalise, et ce qui ne se normalise pas

Les documents de configuration de Cloudflare décrivent ce comportement avec trois exemples. Les chemins entrants tels que cloudflared les reçoit :

Chemin entrant Utilisé pour le matching des ingress Envoyé à l’origine
/hello/../path /path /hello/../path
/hello//path /hello/path /hello//path
/hello\path /hello/path /hello\path

En mots : les dot segments (. et ..) sont supprimés selon RFC 3986 section 5.2.4, les antislash deviennent des slash, et les slashes répétés sont fusionnés.

Remarquez ce que la documentation n’indique pas : le décodage des pourcentages, la gestion des slash en fin de chemin, les paramètres de chemin par point-virgule et le changement de casse. Cela compte car RFC 3986 fait une distinction stricte ici. Un caractère non réservé percent-encodé tel que %61 (la lettre « a ») est équivalent à sa forme littérale, de sorte que les normalisateurs peuvent le décoder. Un caractère réservé percent-encodé n’est pas équivalent : %2F n’est pas la même chose que /, et un normalisateur conforme doit éviter de les fusionner.

Mon constat : traitez tout ce qui échappe aux trois transformations documentées par Cloudflare comme « non normalisé » jusqu’à ce que vos propres tests montrent le contraire. J’ai vérifié cela par rapport à la documentation de Cloudflare, et non en instrumentant cloudflared, donc la matrice de tests ci-dessous est la façon de confirmer le comportement sur votre version.

Pourquoi localhost et un tunnel divergent

Accéder à localhost:8080/admin implique un seul parseur de chemin : celui de votre framework. Une requête tunnelisée passe par au moins trois parseurs :

  1. Le edge de Cloudflare, qui dispose de sa propre fonctionnalité de normalisation d’URL. Selon la documentation de Cloudflare, la normalisation d’URL entrante est activée par défaut, et certaines étapes de base (fusion des barres obliques adjacentes, par exemple) sont toujours appliquées et ne peuvent pas être désactivées.
  2. cloudflared, qui correspond le chemin avec vos règles d’ingress.
  3. Votre origine, dont le framework, le reverse proxy ou le gestionnaire de fichiers statiques applique sa propre décodage et logique de dot-segments.

Il existe un quatrième participant facile à négliger : votre client de test. Les navigateurs résolvent les segments .. avant d’envoyer la requête, et l’analyseur d’URL WHATWG traite les antislash comme des slashes pour les URL HTTP et HTTPS. Ainsi cliquer autour dans un navigateur n’exerce presque jamais ces entrées. Même curl fait s’écraser les dot segments sauf si vous passez --path-as-is. Lorsque vous « testez » un chemin et qu’il fonctionne, le client peut l’avoir réécrit en une forme sûre d’abord.

Tout désaccord entre les parseurs est un contournement potentiel. Si la couche deux pense que la requête est pour /public/... et la couche trois pense qu’elle est pour /admin, une règle écrite pour l’une est évaluée par rapport à l’autre.

La dérivation du chemin non confondante, étape par étape

Considérons un tunnel qui protège /admin avec une validation Access à cloudflared, en profondeur derrière votre politique 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

Les règles d’ingress sont évaluées du premier au dernier, et path est un motif régulier analysé avec le package Go regexp/syntax. Envoyez maintenant GET /public/../admin/users.

  • Normalisation désactivée : le matcher voit /public/../admin/users. Le premier motif assume que le chemin doit commencer par /admin, il ne correspond donc pas. La requête passe au deuxième motif, qui n’a pas d’exigence Access, et est transférée avec son chemin d’origine.
  • Votre origine transforme les dot segments, route la requête vers /admin/users, et le sert. Le contrôle JWT destiné à protéger /admin ne s’est jamais exécuté.
  • Normalisation activée : le matcher voit /admin/users, sélectionne le premier motif et exige un JWT Access valide. L’origine reçoit tout de même le chemin d’origine.

Le schéma est générique : une décision de sécurité prise sur une interprétation d’un chemin, avec l’action prise sur une autre. C’est exactement ce que la recherche d’Orange Tsai (Black Hat USA 2018), « Breaking Parser Logic », a répertorié. Ses diapositives montraient que /foo;name=orange/bar/ est lu comme /foo;name=orange/bar/ par Apache, Nginx et IIS, mais comme /foo/bar/ par Tomcat et Jetty, et utilisent cet écart pour contourner les ACL côté proxy.

Cela continue de se produire, y compris cette année :

  • Heimdall (CVE-2026-42274): avant la version 0.17.14, le proxy aware d’identité correspondait les règles sur le chemin brut tandis que les composants en aval normalisaient les dot segments, ce qui pouvait autoriser une requête sur un chemin et la traiter comme un autre.
  • Quarkus (CVE-2026-50559): les politiques d’autorisation basées sur le chemin pouvaient être contournées avec des points-virgules percent-encodés (%3B), des barres obliques (%2F) ou des antislash (%5C), car la couche de sécurité et la couche de routage ou de fichiers statiques les décodaient différemment.
  • @opennextjs/cloudflare (CVE-2026-3125): un backslash dans /cdn-cgi\image/ a échappé à l’interception du edge de Cloudflare, puis la classe URL d’un Worker l’a converti en slash, atteignant un gestionnaire vulnérable. Corrigé dans la version 1.17.1.
  • fast-uri (CVE-2026-6321): la bibliothèque décodait les séparateurs percent-encodés et les dot segments avant de supprimer les dot segments, de sorte que des URI distincts pouvaient s’effondrer sur un seul chemin normalisé. Corrigé dans 3.1.1.

Des piles différentes, même cause racine : deux parseurs, un seul chemin.

Les quatre entrées à tester

Segments de points (/../ et /./)

Ceci est le cas classique de confusion de traversal et celui que le nouveau réglage de cloudflared traite directement. Testez simplement .. et les formes encodées %2e%2e et %2E%2E. La RFC 3986 indique que les caractères non réservés encodés en pourcentage sont équivalents à leurs formes littérales, et l’analyseur WHATWG traite %2e comme un point dans les segments de chemin. De nombreuses origines décodent %2e avant de supprimer les points, comme le montre l’avis Quarkus. Cloudflare documente la suppression des dot-segments mais ne dit pas s’ils sont décodés en premier, donc cette ligne nécessite un vrai test.

Doubles slashes (//)

Les frameworks difèrent sur le fait que //admin équivaut à /admin. Certains les fusionnent, certains considèrent le segment vide comme significatif, et certains les rejettent. La normalisation de Cloudflare par cloudflared les fusionne uniquement pour le matching. Un motif comme ^/admin ne correspondra pas à //admin sans normalisation, et un origine qui les fusionne les servira volontiers sur /admin.

Slashes finaux (/admin vs /admin/)

La normalisation documentée ne mentionne pas les slashes finaux, il faut donc supposer qu’ils ne sont pas modifiés. Cela rend décisif l’ancrage par expression régulière : ^/admin$ correspond à /admin mais pas à /admin/, tandis que de nombreux frameworks (Express par exemple) routent les deux vers le même gestionnaire. Écrivez les motifs protégés comme ^/admin(/|$), ou mieux, protégez toute l’hôte et isolez les exceptions publiques.

Slashes encodés (%2F) et amis

%2F est là où les parseurs se séparent le plus. Certaines origines le décodent en un vrai séparateur avant le routage, d’autres le conservent dans un seul segment, et certaines le rejettent carrément (beaucoup de serveurs exposent un réglage à ce sujet). Parce que RFC 3986 le traite comme différent de /, la normalisation documentée par cloudflared n’a pas de raison évidente de le décoder, et un normalisateur conforme pourrait le traiter comme un seul segment opaque. Votre origin pourrait ne pas. Il en va de même pour %5C (backslash) et %3B (point-virgule). Le cas Quarkus montre comment une faille peut se nicher exactement dans cet écart.

Une matrice de tests que vous pouvez exécuter dès aujourd’hui

Supposez la configuration ci-dessus, avec l’origine qui renvoie 200 sur /admin/* uniquement lorsque un JWT Access est présent. Pour chaque requête, l’issue est qu’elle est refusée de la même manière que /admin/users est refusé.

Chemin demandé Correspondu (off) Correspondu (on) Documenté ?
/admin/users /admin/users /admin/users Baseline
/public/../admin/users brut /admin/users Oui
//admin/users brut /admin/users Oui
/admin//users brut /admin/users Oui
\admin\users brut /admin/users Oui
/admin/ brut brut Slash final non répertorié
/%61dmin/users brut Non documenté Non, testez-le
/admin%2Fusers brut Non documenté Non, testez-le
/public/%2e%2e/admin/users brut Non documenté Non, testez-le
/admin;x=1/users brut Non documenté Non, testez-le

Deux avertissements lors de l’interprétation des résultats. Premièrement, la normalisation propre de Cloudflare est une couche séparée. Certaines lignes peuvent sembler identiques avec la normalisation cloudflared activée ou désactivée car ledge les réécrit déjà ; c’est acceptable, mais vous voulez savoir quelle couche fait le travail. Deuxièmement, cloudflared tunnel ingress rule <URL> montre quelle règle correspond à une URL, mais la documentation Cloudflare ne dit pas si elle respecte le drapeau de normalisation, alors confirmez aussi avec du trafic réel.

Voici un petit utilitaire Python qui envoie les chemins tels tels, ce que curl et les navigateurs n’effectuent pas toujours :

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} -> {conn.getresponse().status}")
    conn.close()

Réalisez-le deux fois : une fois avec le tunnel sur les valeurs actuelles par défaut, une fois avec --disable-path-normalization=false. Tout chemin qui renvoie autre chose que le refus de référence est une constatation. Pour des vérifications ponctuelles, curl --path-as-is empêche curl de réduire les dot segments avant l’envoi.

Liste de vérification du déploiement avant le 2027-01-25

  1. Déterminez si vous êtes dans le périmètre. Recherchez dans vos configs les blocs access: avec required: true. Aucune règle de ce type, et Cloudflare affirme que vous êtes absent d’effet.
  2. Vérifiez où se situe le bloc access. Un rapport sur GitHub indique que sur cloudflared 2026.7.1 un bloc originRequest.access au niveau supérieur n’était pas appliqué, alors que le même bloc sous une règle d’ingress individuelle l’était. Il s’agit d’un rapport utilisateur, pas d’une documentation confirmée, alors vérifiez sur votre version plutôt que de vous fier à une lecture.
  3. Exécutez la matrice en staging avec la normalisation désactivée, puis activée. Enregistrez chaque différence.
  4. Auditez les ancres des expressions régulières. Remplacez les motifs non ancrés ou se terminant par $ par des motifs qui gèrent les slashes fin.
  5. Activez-la tôt, explicitement. Définir le drapeau maintenant signifie que le changement de janvier est une opération sans effet pour vous. Après le basculement, consultez les notes de version pour confirmer que le drapeau est toujours honoré.
  6. Contrôlez les mises à jour automatiques (--no-autoupdate, verrouillage de version) jusqu’à ce que la matrice passe sur la nouvelle version.
  7. Réparez aussi l’origine. La normalisation au niveau du tunnel est une défense en profondeur. Faites en sorte que votre application rejette les chemins ambigus (slashes encodés, backslashes, dot segments) plutôt que de dépendre du layer en amont.
  8. Journalisez les cibles de requête brutes afin qu’une tentative de sondage apparaisse comme %2e%2e ou \ dans vos journaux, et non comme un chemin nettoyé.

FAQ

Est-ce que cela change le chemin que reçoit mon application ? Non. Cloudflare indique que le chemin URL envoyé à votre origine reste le même. Seul le chemin utilisé pour sélectionner une règle d’ingress est normalisé.

Est-ce que mon tunnel va casser le 2027-01-25 ? Seulement si vous avez des règles nécessitant une validation Access et que votre correspondance repose sur des chemins non normalisés. Cloudflare indique que les configurations sans de telles règles sont non affectées, mais recommande de tester le routage basé sur le chemin avant la date.

La normalisation au niveau du tunnel suffit-elle pour arrêter les bypass de confusion de chemin ? Elle comble la faille pour les transformations qu’elle effectue. Elle ne rend pas l’analyse du chemin de votre origine identique à celle du tunnel, et Cloudflare ne documente pas la décodage des caractères réservés comme %2F. Considérez-la comme une seule couche.

Le même risque existe-t-il avec d’autres tunnels et reverse proxies ? Oui. Tout composant qui prend une décision de routage ou d’autorisation sur un chemin qu’il n’a pas normalisé comme le prochain composant est exposé. Exécutez la matrice contre tout ce qui se trouve à votre edge.

La vérification de signature webhook derrière les tunnels est un mode de défaillance distinct, couvert dans l’article précédent sur les signatures.

Sources

Continue from this article into the most relevant product guides and workflows.

Related Topics

#cloudflared path normalization#cloudflared URL normalization#cloudflare tunnel path normalization#cloudflared ingress rules#cloudflared ingress path matching#cloudflare tunnel routing#cloudflare tunnel ingress#cloudflared 2027 update#cloudflared default normalization#cloudflared changelog#route works on localhost but not through tunnel#localhost vs tunnel routing#tunnel path mismatch#tunnel edge routing#URL normalization#path normalization#URL path normalization security#path normalization bypass#path confusion attack#path confusion auth bypass#path traversal normalization#encoded slash %2F#%2F in URL path#percent encoded slash#URL encoding bypass#dot-segment removal#dot segments in URL#double slash in URL path#trailing slash routing#trailing slash mismatch#path canonicalization#URL canonicalization#RFC 3986 normalization#reverse proxy path normalization#reverse proxy routing rules#ingress rule matching#ingress path regex#access rules bypass#authentication bypass URL#authorization bypass path#access control path matching#Cloudflare Access path rules#WAF path normalization#API gateway path matching#HTTP request smuggling path#proxy and origin parser differential#path parsing differences#normalization test matrix#URL normalization testing#tunnel routing test cases#cloudflared config.yml#cloudflared hostname path rules#expose localhost securely#localhost tunneling#localhost tunnel security#developer tunneling tools#DevOps routing best practices#cloud-native networking#web security for developers

Keep building with InstaTunnel

Read the docs for implementation details or compare plans before you ship.

Share this article

More InstaTunnel Insights

Discover more tutorials, tips, and updates to help you build better with localhost tunneling.

Browse All Articles