Normalización de rutas en el borde del túnel: por qué una ruta coincide en localhost pero no a través de un túnel
Por qué las rutas coinciden en localhost pero no a través de un Cloudflare Tunnel. Vea cómo la normalización de la ruta de cloudflared cambia %2F, segmentos con puntos, barras y reglas de ingress.

Quick answer
Normalización de rutas con cloudflared: por qué las rutas fallan en túneles: 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.
Tu ruta /admin funciona en localhost:8080. A través del túnel, //admin devuelve algo distinto, /public/../admin se comporta de forma distinta, y una regla que asumías que protegía la ruta nunca parece activarse. No hay nada malo en tu aplicación. Varios componentes están analizando la misma ruta con reglas diferentes, y un túnel añade un analizador más a la cadena.
Esa cadena está a punto de cambiar. A partir del 2027-01-25, las nuevas versiones de cloudflared activarán la normalización de URL por defecto, lo que Cloudflare dice que podría cambiar qué regla de ingress coincide con una solicitud. Esta guía explica qué cambia, qué no, cómo funcionan las evasiones de autenticación por confusión de rutas y cómo probar tu propio enrutamiento antes de que el comportamiento por defecto cambie.
\u003e Resumen rápido
\u003e
\u003e - cloudflared 2026.10.0 y versiones posteriores pueden normalizar la ruta de la URL antes de hacer coincidir las reglas de ingress.
\u003e - Está desactivada por defecto hoy y activada por defecto en nuevas versiones a partir de 2027-01-25.
\u003e - Cloudflare limita el cambio a túneles con reglas de ingress que requieren validación de Cloudflare Access; otras configuraciones no se ven afectadas.
\u003e - Las transformaciones documentadas son la eliminación de dot-segments, la conversión de barras invertidas a barras diagonales y la fusión de barras repetidas. La ruta enviada a tu origen no se modifica.
\u003e - La codificación por porcentaje (como %2F) y las barras finales no están en la lista documentada, así que pruébalas en lugar de asumir.
Qué anunció Cloudflare
El 22 de octubre de 2026, Cloudflare publicó una entrada del changelog titulada “Cloudflare Tunnel supports URL normalization.” Los hechos clave:
- Disponibilidad:
cloudflaredversión2026.10.0y posteriores. GitHub lista 2026.10.0 como publicada el 5 de octubre de 2026; la versión más reciente que pude encontrar al 5 de noviembre de 2026 es 2026.10.6. - Predeterminado hoy: desactivado. El flag está invertido, por lo que el predeterminado documentado de
--disable-path-normalizationestrue. - Predeterminado a partir de 2027-01-25: activado, para nuevas versiones.
- Alcance: Cloudflare dice que mejora la seguridad para túneles con reglas que requieren validación de Cloudflare Access, y que configuraciones sin tales reglas no se ven afectadas.
- Ruta de origen: sin cambios. La normalización se aplica a la ruta utilizada para coincidir, no a la ruta enviada al origen.
Para activarlo ahora:
cloudflared tunnel --disable-path-normalization=false run <TUNNEL_NAME_OR_UUID>
o establecer TUNNEL_DISABLE_PATH_NORMALIZATION=false en cada réplica de cloudflared y reiniciarla.
Un detalle operativo merece atención. Los binarios autónomos de cloudflared que se ejecutan como servicio se actualizan automáticamente cada 24 horas por defecto (autoupdate-freq). Las instalaciones por gestor de paquetes, Windows y ejecuciones interactivas no se actualizan automáticamente. Si tu túnel se actualiza automáticamente, o despliegas una etiqueta de imagen latest flotante, el nuevo predeterminado llegará sin una actualización deliberada. Ya sea estableciendo el flag explícitamente ahora o fijando tu versión hasta que la pruebes.
Qué se normaliza y qué no
Los documentos de configuración de Cloudflare describen el comportamiento con tres ejemplos. Las rutas entrantes se muestran tal como las recibe cloudflared:
| Ruta entrante | Usado para la coincidencia de ingress | Enviado al origen |
|---|---|---|
/hello/../path |
/path |
/hello/../path |
/hello//path |
/hello/path |
/hello//path |
/hello\\path |
/hello/path |
/hello\\path |
En palabras: los segmentos con puntos (. y ..) se eliminan siguiendo RFC 3986, sección 5.2.4; las barras invertidas se convierten en barras diagonales y las barras repetidas se fusionan.
Observa lo que la documentación no lista: decodificación de porcentajes, manejo de barras finales, parámetros de ruta con punto y coma y la normalización de mayúsculas/minúsculas. Esto importa porque RFC 3986 traza una línea rígida aquí. Un carácter codificado con porcentaje no reservado como %61 (la letra “a”) es equivalente a su forma literal, por lo que los normalizadores pueden decodificarlo. Un carácter codificado con porcentaje reservado no es equivalente: %2F no es lo mismo que /, y un normalizador conforme a estándares no debe colapsarlos.
Mi conclusión: trate cualquier cosa fuera de las tres transformaciones documentadas por Cloudflare como “no normalizado” hasta que su propia prueba muestre lo contrario. Verifiqué esto frente a la documentación de Cloudflare, no instrumentando cloudflared, por lo que la matriz de pruebas a continuación es la forma de confirmar el comportamiento en su versión.
Por qué localhost y un túnel difieren
Al hacer localhost:8080/admin interviene un analizador de ruta: el de tu framework. Una solicitud a través del túnel pasa al menos por tres:
- El edge de Cloudflare, que tiene su propia característica de normalización de URL. Según la documentación de Cloudflare, la normalización de URL entrante está activada por defecto, y ciertos pasos de base (fusionar barras adyacentes, por ejemplo) siempre se aplican y no se pueden desactivar.
cloudflared, que compara la ruta con tus reglas de ingress.- Tu origen, cuyo framework, proxy inverso o manejador de archivos estáticos aplica su propia decodificación y lógica de dot-segments.
Hay un cuarto participante, fácil de pasar por alto: tu cliente de prueba. Los navegadores resuelven los segmentos .. antes de enviar una solicitud, y el analizador URL WHATWG trata las barras invertidas como barras diagonales para URLs http y https. Así que hacer clic en un navegador casi nunca ejercita estas entradas. Incluso curl colapsa los dot segments a menos que pases --path-as-is. Cuando “probaste” una ruta y funcionó, es posible que el cliente la haya reescrito primero a una forma segura.
Cualquier desacuerdo entre analizadores es una posible bypass. Si la capa dos piensa que la solicitud es para /public/... y la capa tres piensa que es para /admin, una regla escrita para una se evalúa contra la otra.
La elusión de autenticación por confusión de ruta, paso a paso
Considere un túnel que protege /admin con validación de Access en cloudflared, defensa en profundidad detrás de su política de 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
Las reglas de ingress se evalúan de primero a último, y path es una expresión regular analizada con el paquete regexp/syntax de Go. Ahora envía GET /public/../admin/users.
- Normalización desactivada: el matcher ve
/public/../admin/users. La expresión regular de la primera regla exige que la ruta empiece con/admin, así que no coincide. La solicitud pasa a la segunda regla, que no tiene requisito de Access, y se reenvía con su ruta original. - Tu origen colapsa el segmento con puntos, dirige la solicitud a
/admin/users, y la sirve. La verificación JWT que se suponía debía proteger/adminnunca se ejecutó. - Normalización activada: el matcher ve
/admin/users, selecciona la primera regla y exige un JWT de Access válido. El origen aún recibe la ruta original.
El patrón es genérico: una decisión de seguridad tomada sobre una interpretación de una ruta, con la acción tomada en otra. Esto es exactamente lo que la investigación de Orange Tsai, Black Hat USA 2018 y DEF CON, “Breaking Parser Logic”, catalogó. Sus diapositivas mostraron que /foo;name=orange/bar/ se lee como /foo;name=orange/bar/ por Apache, Nginx e IIS, pero como /foo/bar/ por Tomcat y Jetty, y usaron esa brecha para eludir ACLs a nivel de proxy.
Sigue ocurriendo, incluso este año:
- Heimdall (CVE-2026-42274): antes de la versión 0.17.14, el proxy con identidad resolvía reglas sobre la ruta cruda mientras que los componentes downstream normalizaban dot segments, por lo que una solicitud como
/user/../adminpodría ser autorizada como una ruta y procesada como otra. - Quarkus (CVE-2026-50559): las políticas de autorización basadas en la ruta podían ser eludidas con semicolones codificados por porcentaje (
%3B), barras (%2F) o barras invertidas (%5C), porque la capa de seguridad y la capa de enrutamiento o de archivos estáticos las decodificaban de forma diferente. - @opennextjs/cloudflare (CVE-2026-3125): una barra invertida en
/cdn-cgi\\image/se escapó de la intercepción en el edge de Cloudflare, luego la clase URL de Worker’s la convirtió en una barra diagonal, alcanzando un manejador vulnerable. Corregido en 1.17.1. - fast-uri (CVE-2026-6321): la biblioteca decodificaba separadores codificados por porcentaje y dot segments antes de eliminar dot segments, por lo que diferentes URIs podían colapsar en una ruta normalizada. Arreglado en 3.1.1.
Diferentes pilas, la misma causa raíz: dos analizadores, una ruta.
Las cuatro entradas para probar
Segmentos con puntos (/../ y /./)
Este es el clásico caso de confusión de traversal, y es a lo que la nueva configuración de cloudflared aborda directamente. Prueba con .. simple y las formas codificadas %2e%2e y %2E%2E. RFC 3986 dice que los caracteres no reservados codificados por porcentaje son equivalentes a sus literales, y el analizador WHATWG URL trata %2e como un punto en los segmentos de ruta. Muchos orígenes decodifican %2e antes de eliminar puntos, como muestra el aviso de Quarkus. Cloudflare documenta la eliminación de dot-segments pero no dice si los puntos codificados se decodifican primero, así que esta fila necesita una prueba real.
Barras dobles (//)
Los frameworks difieren en si //admin es igual a /admin. Algunos fusionan, algunos tratan el segmento vacío como significativo, y algunos lo rechazan. La normalización de cloudflared de Cloudflare los fusiona solo para la coincidencia. Un patrón como ^/admin no coincidirá con //admin sin normalización, y un origen que los fusione servirá con /admin sin problemas.
Barras finales (/admin vs /admin/)
La normalización documentada no menciona las barras finales, así que asume que no se tocan. Eso hace decisiva la anclaje de expresiones regulares: ^/admin$ coincide con /admin pero no con /admin/, mientras que muchos frameworks (Express por defecto, por ejemplo) enrutan ambos a la misma manejador. Escribe patrones de rutas protegidas como ^/admin(/|$) o, mejor, protege todo el host y reserva excepciones públicas.
Barras codificadas (%2F) y compañía
%2F es donde la mayoría de los analizadores separan la ruta. Algunas fuentes la decodifican en un separador real antes de enrutar, otras la mantienen dentro de un único segmento, y otras la rechazan por completo (muchos servidores exponen una configuración para esto). Dado que RFC 3986 la trata como no equivalente a /, la normalización documentada de cloudflared no tiene una razón obvia para decodificarla, y un comparador que siga los estándares no debe colapsarla. Su origen podría no hacerlo. Lo mismo ocurre con %5C (barra invertida) y %3B (punto y coma). El caso de Quarkus muestra cómo una bypass puede esconderse precisamente en esa brecha.
Una matriz de pruebas que puedes ejecutar hoy
Asumiendo la configuración anterior, con el origen devolviendo 200 en /admin/* solo cuando hay un JWT de Access presente. Para cada solicitud, la condición de aprobación es que se deniegue de la misma manera que /admin/users.
| Ruta de la solicitud | Coincidió como (apagado) | Coincidió como (encendido) | ¿Documentado? |
|---|---|---|---|
/admin/users |
/admin/users |
/admin/users |
Baseline |
/public/../admin/users |
crudo | /admin/users |
Sí |
//admin/users |
crudo | /admin/users |
Sí |
/admin//users |
crudo | /admin/users |
Sí |
\\admin\\users |
crudo | /admin/users |
Sí |
/admin/ |
crudo | crudo | Barra final no listada |
/%61dmin/users |
crudo | No documentado | No, pruébalo |
/admin%2Fusers |
crudo | No documentado | No, pruébalo |
/public/%2e%2e/admin/users |
crudo | No documentado | No, pruébalo |
/admin;x=1/users |
crudo | No documentado | No, pruébalo |
Dos precauciones al leer los resultados. Primero, la normalización propia del edge de Cloudflare es una capa separada. Algunas filas pueden parecer idénticas con la normalización de cloudflared encendida o apagada porque el edge ya las reescribió; está bien, pero quieres saber qué capa está haciendo el trabajo. En segundo lugar, cloudflared tunnel ingress rule <URL> muestra qué regla coincide con una URL, pero la documentación de Cloudflare no dice si respeta el flag de normalización, así que confirma también con tráfico en vivo.
Aquí tienes un pequeño entorno Python que envía las rutas tal como están escritas, lo que curl y los navegadores no siempre hacen:
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()
Ejecutarlo dos veces: una con el túnel activado según los valores por defecto actuales, y otra con --disable-path-normalization=false. Cualquier ruta que devuelva algo distinto a la denegación base es un hallazgo. Para comprobaciones puntuales, curl --path-as-is evita que curl colapse los dot segments antes de enviar.
Lista de verificación de implementación antes del 2027-01-25
- Determina si estás en el alcance. Busca en tus configuraciones bloques
access:conrequired: true. No hay reglas de este tipo, y Cloudflare dice que no te afecta. - Comprueba dónde se ubica el bloque
access. Un informe abierto en el repositorio de documentación de Cloudflare señala que encloudflared2026.7.1 un bloqueoriginRequest.accessde nivel superior no se hacía cumplir, mientras que el mismo bloque bajo una regla de ingress individual sí lo estaba. Esto es un informe de usuario, no una documentación confirmada, así que verifica en tu versión en lugar de confiar en una lectura. - Ejecuta la matriz en staging con la normalización desactivada, luego activada. Registra cada diferencia.
- Audita los anclajes de expresiones regulares. Reemplaza patrones no anclados o terminados en
$para rutas protegidas por otros que manejen las barras finales. - Actívalo temprano, explícitamente. Establecer la bandera ahora significa que el cambio de enero no afectará tu entorno. Después de la migración, consulta las notas de la versión para confirmar que la bandera sigue siendo respetada.
- Controla las actualizaciones automáticas (
--no-autoupdate, fijación de versión) hasta que la matriz funcione en la nueva versión. - Corrige también el origen. La normalización en el túnel es defensa en profundidad. Haz que tu aplicación rechace rutas ambiguas (barras codificadas, barras invertidas, dot segments) en lugar de depender de la capa que está delante.
- Registra los objetivos de solicitud en crudo para que un intento de sondeo aparezca como
%2e%2eo\en tus registros, y no como una ruta limpiada.
Preguntas frecuentes
¿Este cambio modifica la ruta que recibe mi aplicación? No. Cloudflare afirma que la ruta URL enviada a tu origen permanece igual. Solo la ruta utilizada para seleccionar una regla de ingress se normaliza.
¿Mi túnel dejará de funcionar el 2027-01-25? Solo si tienes reglas que requieren validación de Access y tu coincidencia depende de rutas no normalizadas. Cloudflare dice que las configuraciones sin esas reglas no se ven afectadas, pero recomienda probar el enrutamiento basado en rutas antes de la fecha.
¿Es suficiente la normalización en el túnel para detener las elusiones por confusión de rutas?
Cierra la brecha para las transformaciones que realiza. No hace que el análisis de tu origen sea consistente con el del túnel, y Cloudflare no documenta la decodificación de caracteres reservados como %2F. Considéralo como una única capa.
¿Existe el mismo riesgo con otros túneles y proxies inversos? Sí. Cualquier componente que tome una decisión de enrutamiento o autorización sobre una ruta que no ha normalizado como lo hará el siguiente componente está expuesto. Ejecuta la matriz contra lo que esté en tu edge.
La verificación de firmas de webhook detrás de túneles es un modo de fallo separado, cubierto en la publicación anterior sobre firmas.
Fuentes
- 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.