Skip to main content

Vulnerabilidades de confusión de URL en la práctica: exploración de inconsistencias entre analizadores

Escrito por
Headshot of Snyk Security Research Team

Snyk Security Research Team

Claroty Team82

feature url confusion claroty

10 de enero de 2022

0 minutos de lectura

Las URL han cambiado para siempre la forma en que interactuamos con las computadoras. Concebidas en 1992 y definidas en 1994, las direcciones URL (Uniform Resource Locator) siguen siendo un componente fundamental de Internet, ya que permiten navegar por la web mediante direcciones descriptivas y comprensibles para las personas. Pero la necesidad de que fueran legibles para las personas también hizo necesario dividirlas en componentes que las máquinas pudieran usar; de eso se encargan los analizadores de URL.

Dada la ubicuidad de las URL durante décadas, uno pensaría que los analizadores que usamos hoy habrían llegado a un consenso sobre cómo analizar URL similares (o incluso idénticas). Aunque sería lo ideal, la realidad está muy lejos de eso. Esta falta de consenso ha contribuido a una clase de vulnerabilidad llamada confusión de URL, que exploraremos en esta publicación.

Nota: El proceso de desarrollo de los protocolos de Internet (HTTP, HTML, URL, etc.) usa documentos conocidos como Requests for Comment o RFC. En esta publicación, haremos referencia a estos documentos con frecuencia.

En nuestra investigación conjunta, examinamos muchas bibliotecas de análisis de URL en distintos lenguajes de programación y observamos algunas inconsistencias en la forma en que cada una dividía las URL en sus componentes básicos. Clasificamos los tipos de inconsistencias en cuatro categorías y buscamos flujos de código problemáticos tanto en aplicaciones web como en bibliotecas de código abierto. Estos esfuerzos revelaron numerosas vulnerabilidades, la mayoría con una de estas causas principales:

  1. Se usan varios analizadores de URL

  2. Con el tiempo, hubo varios RFC relacionados con las URL, y distintos analizadores implementaron distintos RFC

En esta publicación, repasaremos la historia de las URL, exploraremos posibles fuentes de confusión entre analizadores de URL, veremos una prueba de concepto de explotación y, luego, ofreceremos recomendaciones para protegerte de los ataques basados en la confusión de URL. Usa la tabla de contenido a continuación para ir directamente a una sección:

  1. ¿Qué son las URL y cómo llegamos hasta aquí?

  2. ¿Dónde se confunden los analizadores de URL?

  3. Prueba de concepto de explotación (y CVE asignados)

  4. Recomendaciones

La investigación conjunta de los equipos de investigación de seguridad de Claroty y Snyk se inspira en trabajos anteriores de Orange Tsai, titulados «A New Era of SSRF», y en una comparación entre WHATWG y RFC 3986, realizada por Daniel Stenberg, creador de cURL. Queremos agradecerles por su innovadora investigación.

¿Qué son las URL y cómo llegamos hasta aquí?

Cuando pensamos en una URL, pensamos en algo como https://snyk.io. En su forma más básica, indica adónde ir (my-site.com) y cómo llegar (HTTPS). Además de esos componentes, también podemos encontrar elementos adicionales, como rutas (https://my-site.com/about), símbolos de numeral seguidos de una cadena (fragmentos; por ejemplo, https://my-site.com#contact) o signos de interrogación seguidos de parámetros (consultas; por ejemplo, https://my-site.com?source=li&device=mobile).

Si generalizamos y combinamos lo anterior, podemos dividir una cadena de URL en los siguientes componentes principales:

scheme://authority/path?query#fragment

Por ejemplo, una URL puede verse así: https://example.com:8042/over/there?name=ferret#nose

Durante nuestra investigación, analizamos cómo varios analizadores implementaban distintos RFC. Aunque el formato de URL anterior parece relativamente sencillo, los RFC que lo definieron han cambiado mucho desde 1994. Estas adiciones y revisiones dieron forma a la URL que conocemos hoy.

Cronología de los estándares de URL: desde RFC 1738 en 1994 hasta RFC 3986 en 2005, con revisiones para URL relativas, URN e IPv6.

Más información: RFC 1738, RFC 1808, RFC 2141, RFC 2396, RFC 2732, RFC 3986

Para entender dónde se confunden los analizadores, primero debemos revisar (brevemente) qué significa cada componente de una URL completa y cómo se define.

Esquema

La sección del esquema es donde se define el protocolo (es decir, HTTP, HTTPS, FTP, Gopher, etc.). Los RFC definen qué caracteres se permiten en este componente y establecen que el esquema actual debe ajustarse al patrón ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ). Veamos cómo funciona:

  • Primer carácter: a–Z

  • Caracteres que no son el primero: a–Z, 0–9, +, -, .

Esto significa que 1http no es un esquema válido (porque empieza con un número), pero h2ttp sí lo es (porque el primer carácter debe ser una letra). Además, a diferencia de los demás componentes de una URL, el esquema es el único obligatorio.

Autoridad (antes, Netloc)

Antes, este componente se llamaba «Netloc» (network-location) y luego pasó a llamarse Authority. Está compuesto por tres subcomponentes internos: userinfo, host y port. En conjunto, un ejemplo generalizado de una autoridad se ve así: authority = [ userinfo "@" ] host [ ":" port ]. El subcomponente userinfo también se puede reducir a sus elementos principales de la siguiente manera: username[:password]. Esto permite manejar URL con el formato scheme://user:password@domain:port/. Ten en cuenta que, desde RFC-2396, se desaconsejó el uso de user:password en texto sin formato, y que quedó obsoleto en RFC-3986.

Ruta

Este componente indica a qué recursos del servidor quiere acceder quien realiza la solicitud. Puede contener cualquier secuencia de caracteres (excepto /,;,=,?) dividida en segmentos (delimitados por /).

Aunque parece ser el componente más sencillo, también es el que más cambios ha tenido a lo largo de los años. RFC-1738 y RFC-1808 vincularon la estructura y las reglas del componente de ruta con el componente de esquema. Después, RFC-2396 estableció que cada segmento de ruta podía usar el carácter de punto y coma (;) para definir parámetros. Sin embargo, poco después de RFC-2396 llegó RFC-3986 y la sintaxis de parámetros con punto y coma quedó obsoleta.

¿Confuso? Pues los analizadores también se confunden.

Consulta

Las consultas son pares clave-valor que se envían mediante la URL al recurso solicitado. Los parámetros de consulta aparecen después del primer signo de interrogación (?) y terminan con un signo de numeral/hash (#).

Dentro de un componente de consulta, el punto y coma (;), la barra (/), el signo de interrogación (?), los dos puntos (:), el símbolo arroba (@), el ampersand (&), el signo igual (=), el signo más (+), la coma (,) y el signo de dólar ($) son caracteres reservados y se codifican en URL cuando se usan.

Este es un ejemplo de codificación de caracteres especiales en una URL:

Fragmento

El fragmento se usa para identificar y acceder a un segundo recurso dentro del primer recurso obtenido, especificado por el componente de ruta. La presencia de un numeral (#) indica un componente de fragmento, que termina al final del URI.

... ¡y eso es todo sobre los componentes!

URL relativas

Lo último que veremos en esta sección es el aspecto de las referencias relativas en las URL. Como las URL son jerárquicas por naturaleza, una puede ser relativa a otra. Por ejemplo, dada una URL «base» https://example.com/ y un segmento de cadena de URL que empieza con una ruta, como /foo/bar, el analizador los resolverá como https://example.com/foo/bar; sin embargo, primero se le debe proporcionar la URL «base».

Esto significa que los analizadores deben saber cómo analizar referencias relativas. RFC-3986 define tres tipos de referencias relativas:

  1. Referencia de ruta de red: comienza con //; por ejemplo, //example.com

  2. Referencia de ruta absoluta: comienza con /; por ejemplo, /etc/passwd

  3. Referencia de ruta relativa: no comienza con /; por ejemplo, foo/bar

Tipo de referencia

Ejemplo

Ruta de red

//snyk.io

Ruta absoluta

/etc/passwd

Ruta relativa

app/login.js

Mientras que los dos últimos tipos requieren una URL base para resolverse, la referencia de ruta de red solo requiere que esté presente el esquema. Por lo tanto, si un analizador establece HTTPS como esquema predeterminado, //example.com se convertirá en https://example.com.

¿Dónde se confunden los analizadores de URL?

Después de revisar todas las partes de una cadena de URL y los cambios en los RFC a lo largo de los años, decidimos enfocarnos en los analizadores de URL y buscar casos límite que les hicieran producir resultados de análisis incorrectos o inesperados.

Durante nuestra investigación, revisamos 15 bibliotecas (escritas en distintos lenguajes de programación), herramientas para obtener URL (como curl y wget) y navegadores.

Encontramos muchas inconsistencias entre los analizadores y las clasificamos en cuatro categorías principales. Las categorías que se describen a continuación nos permiten engañar a la mayoría de los analizadores y crear una variedad de comportamientos impredecibles, lo que facilita una amplia gama de vulnerabilidades.

Las categorías que definimos son:

  1. Confusión de esquema

  2. Confusión de barras

  3. Confusión de barras invertidas

  4. Confusión por codificación de URL

¡Sin más preámbulos, veámoslas!

Confusión de esquema

Casi todos los analizadores de URL se confunden cuando no está presente el componente de esquema. La confusión se debe a que RFC 3986 establece que el esquema es la única parte obligatoria de la URL, mientras que RFC 2396 y los anteriores no lo establecen así. Implementar analizadores que intenten ser compatibles con versiones anteriores frente a estos matices no es sencillo y, por eso, se produce la confusión.

Para demostrarlo, aquí hay cuatro bibliotecas de Python distintas a las que se les proporcionó la URL google.com/abc:

Editor de código oscuro que compara los resultados del análisis de la URL «google.com/abc» con urllib, urllib3, RFC 3986 y httptools

Como se muestra arriba, la mayoría de los analizadores indican que el componente de host está vacío cuando reciben la cadena google.com/abc. Sin embargo, Urllib3 indica que el host es google.com y la ruta es /abc. Por su parte, httptools afirma que esta URL no es válida desde el principio. En resumen, casi ningún analizador procesa correctamente esta URL, ya que no cumple con las especificaciones de los RFC.

Dicho esto, algunos analizadores recurren al esquema predeterminado, como hace curl en este caso:

Terminal que muestra una solicitud curl a google.com y una respuesta HTML 301 Moved que apunta a la dirección www.

En este caso, los atacantes pueden aprovechar la diferencia entre analizadores para evadir la validación. Si un analizador de URL intenta validar hosts específicos, pero no puede analizar correctamente la URL para extraer el host y, al mismo tiempo, la biblioteca subyacente sí analiza correctamente la URL (o recurre a un valor predeterminado), se produce una evasión. Por ejemplo:

Código de Python que comprueba una URL contra una lista de bloqueo de localhost antes de enviar una solicitud GET a localhost/secret.txt

En el ejemplo anterior, urllib (específicamente la función urlsplit) analiza la URL sin netloc, por lo que la verificación se aprueba; sin embargo, urllib3 recurre al protocolo predeterminado http y obtiene el recurso prohibido.

Confusión de barras

El siguiente método de confusión tiene que ver con un número no estándar de barras en la URL. RFC 3986 indica que el componente de autoridad de la URL debe aparecer después de dos barras tras dos puntos, ://, y que se extiende hasta el final de la línea (EOL) o hasta que se encuentra un delimitador. En este caso, un delimitador puede ser una barra (que indica un componente de ruta), un signo de interrogación (componente de consulta) o un numeral (componente de fragmento).

Durante la investigación, observamos varios comportamientos de los analizadores al intentar procesar una URL que no sigue la sintaxis anterior. Ante la siguiente URL, http:///google.com, los analizadores se comportaron de forma interesante:

Salida de terminal que compara los resultados del análisis de la URL para la dirección malformada http:///google.com en varias bibliotecas de Python.

Como puedes ver, la mayoría de los analizadores afirmaron que esta URL no tiene host, sino que interpretaron /google.com como una ruta en una URL sin host, que es lo esperado según RFC 3986. Pudimos reproducir este comportamiento con cualquier cantidad de barras después del esquema. Sin embargo, encontramos un grupo de analizadores que intentaban «corregir» la URL e ignoraban las barras adicionales o faltantes (hasta cierto punto). Por ejemplo, la función nativa fetch de JavaScript tratará estas URL como si fueran correctas:

Consola del navegador que muestra una solicitud fetch a https://www.google.com y una Promise resuelta que contiene una respuesta HTML.

Y esto también es cierto para curl:

Terminal que muestra comandos curl con URL de Google mal formadas y corregidas, un error de formato de URL y respuestas 301 Moved.

Estas diferencias en los resultados del análisis crean una amplia superficie de ataque. Al igual que con la idea de ataque anterior (la confusión de esquemas), ¿qué pasaría si pudiéramos eludir las comprobaciones porque el primer analizador interpreta la URL de forma distinta al cliente que la obtiene? Considera este ejemplo:

Editor de código oscuro que muestra el análisis de URL, una verificación de lista de bloqueo para foo.com y un comando curl para obtener datos

De nuevo, vemos una validación de netloc contra un dominio bloqueado y, otra vez, estará vacío debido al análisis, por lo que la comprobación se aprobará. Más adelante en el código, curl interpretará la URL de otra manera e intentará obtener el recurso de forma inesperada. Esto puede provocar SSRF y permitir el acceso a otros hosts no autorizados.

Confusión con barras invertidas

Una variante de la confusión con barras diagonales es la confusión con barras invertidas. Esta confusión puede producirse cuando una URL usa una barra invertida (\) en lugar de una barra diagonal (/) y, por lo tanto, crea una URL mal formada. Según RFC 396, una barra invertida es distinta de una barra diagonal y debe interpretarse de otra manera, por lo que http://google.com es diferente de http:\\google.com. Fieles a la RFC, la mayoría de los analizadores de URL programáticos efectivamente tratan las dos URL anteriores de forma distinta:

Salida de terminal que compara cómo interpretan las bibliotecas de análisis de URL de Python la URL mal formada http:\\google.com

Sin embargo, Chrome opta por interpretar la barra invertida como si fuera una barra diagonal:

Fragmento de código que muestra una respuesta HTTP 302 Found que redirige a https://www.google.com

Navegará a la URL como si fuera válida. Llevando esto al extremo, este comportamiento también ocurre con https:/\google.com, y Chrome mostrará el recurso como (in)esperado.

Confusión con URL codificadas

La última categoría de confusión es la de las URL codificadas. Esta confusión ocurre cuando una URL contiene una subcadena codificada en URL en un lugar donde no se espera.

En términos generales, la codificación URL permite incluir caracteres no imprimibles en las cadenas de URL. Se realiza usando el valor hexadecimal de los caracteres, precedido por el símbolo %; por ejemplo, g es %67 cuando está codificado en URL. Este método permite que las cadenas de URL sigan siendo completamente textuales y visibles, independientemente de los caracteres que contengan. Aunque este método se usa principalmente con caracteres no imprimibles, también se pueden codificar en URL caracteres imprimibles, y ahí es donde surge la confusión.

RFC 3986 establece que todos los componentes de una URL, excepto el esquema, se pueden codificar en URL. Sin embargo, en la práctica, muchos analizadores no analizan el componente netloc.

Estos son los resultados del análisis cuando proporcionamos a los analizadores la URL http://google.com en su versión codificada en URL:

Captura de pantalla de una terminal que compara los resultados del análisis de una URL HTTP con caracteres codificados con porcentajes, incluido un error de URL no válida.

Aunque los resultados anteriores parecen esperables, tanto urllib como requests de Python mostraron un comportamiento interesante cuando se les proporcionó una URL codificada:

Ejemplos de código con Python requests y urllib para acceder a 127.0.0.1 mediante una dirección de loopback codificada.

En ambos casos anteriores, se envió inesperadamente una solicitud a 127.0.0.1.

Por lo tanto, esta discrepancia crea otra superficie de ataque, ya que los patrones Regex simples no detectarán estas cadenas y es posible que podamos volver a eludir las comprobaciones.

Prueba de concepto de explotación (y CVE asignados)

Ahora que entendemos dónde está la confusión, veamos cómo explotar estos problemas y las vulnerabilidades que encontramos. Analizaremos un CVE, pero la lista completa de CVE se encuentra a continuación.

Clearance (Ruby): CVE-2021-23435: vulnerabilidad de redirección abierta

Las vulnerabilidades de redirección abierta se producen cuando una aplicación web acepta una entrada controlada por el usuario que especifica a qué URL se redirigirá al usuario después de una acción determinada (como iniciar sesión). Para ilustrar mejor el ataque, aquí tienes un diagrama que lo muestra:

Diagrama que muestra a un usuario redirigido de foo.com a evil.com, con servidores etiquetados foo.com y evil.com.

Como puedes ver, el atacante proporciona a la víctima una URL para que la use. Con la configuración adecuada, el servidor redirigirá al usuario a un sitio controlado por el atacante.

Clearance es una gema de Ruby cuyo objetivo es ampliar el mecanismo de autenticación del framework Rails mediante la incorporación de autenticación de email-and-password. Después de iniciar o cerrar sesión, redirige al usuario mediante una URL que obtiene de una solicitud anterior del usuario (es decir, el recurso que quien hizo la solicitud pidió antes de llegar a la página de inicio de sesión).

# /authorization.rb
# @api private
    def store_location
      if request.get?
        session[:return_to] = request.original_fullpath
      end
    End

El código vulnerable está dentro de la función return_to (la devolución de llamada posterior al inicio o cierre de sesión):

# @api private
    def return_to
      if return_to_url
        uri = URI.parse(return_to_url)
        "#{path}?#{uri.query}".chomp("?") + "##{uri.fragment}".chomp("#")
      end
    End

Aunque tengamos en cuenta las redirecciones abiertas, return_to no permite que los usuarios indiquen libremente una URL de return_to. Sin embargo, si un usuario solicita un recurso auth-required sin haber iniciado sesión, el sistema llamará a store_location y redirigirá el navegador a la página de inicio de sesión. Por eso, si un atacante logra convencer a una víctima de que haga clic en un enlace como http://target.com/////evil.com, se activará la vulnerabilidad.

¿Por qué se produciría una redirección abierta? ¡Porque hay varios analizadores!

Como vimos, store_location almacena la ruta completa de la URL (esto también se debe a que Ruby ignora las barras diagonales múltiples en las URL). Esto significa que guarda /////evil.com en su caché. Cuando se llama a URI.parse, se eliminan dos barras diagonales adicionales, por lo que ahora tenemos ///evil.com. Cuando un navegador (como Chrome) recibe esta URL, la interpreta como una referencia de ruta de red y redirige al cliente a http://evil.com.

La razón por la que hoy los navegadores suelen «perdonar» estos errores en las URL (e intentar corregirlos) es que, a lo largo de los años, han tenido que lidiar con muchas URL imperfectas o que no cumplen con la RFC. Para ser más tolerantes con los errores de clientes y desarrolladores, con el tiempo han optado por omitir o agregar barras diagonales en los errores más comunes.

Este es un fragmento del código fuente del proyecto Chromium:

// The syntax rules of the two slashes that precede the host in a URL are
// surprisingly complex. They are not required, even if a scheme is included
// (http:example.com is treated as valid), and are valid even if a scheme is
// not included (//example.com is treated as file:///example.com). They can
// even be backslashes (http:\\example.com and http\/example.com are both
// valid) and there can be any number of them (http:/example.com and
// http://////example.com are both valid).
// We will therefore define slashes as a list of enum values (repeated
// Slash). In our conversion code, this will be read to append the
// appropriate kind and appropriate number of slashes to the URL.

Otras vulnerabilidades

Estas son otras vulnerabilidades causadas por diferencias entre analizadores:

  1. Flask-security (Python, CVE-2021-23385)

  2. Flask-security-too (Python, CVE-2021-32618)

  3. Flask-User (Python, CVE-2021-23401)

  4. Flask-unchained (Python, CVE-2021-23393)

  5. Belledonne’s SIP Stack (C, CVE-2021-33056)

  6. Video.js (JavaScript, CVE-2021-23414)

  7. Nagios XI (PHP, CVE-2021-37352)

  8. Clearance (Ruby, CVE-2021-23435)

Recomendaciones para evitar la confusión con URL

Como los problemas analizados en esta publicación se deben principalmente a la presencia de varios analizadores y a sus distintos enfoques, es importante saber cuáles están presentes en tu aplicación al construir este tipo de sistemas. Esto se vuelve más complejo con las arquitecturas actuales (por ejemplo, microservicios y mallas de servicios) y puede tomar tiempo entender por completo qué componentes de análisis hay en tu aplicación y cuál es el recorrido de una solicitud.

Una vez que tengas la lista de analizadores, es fundamental que los desarrolladores comprendan a fondo las diferencias entre sus lógicas de análisis, para poder trabajar con eficiencia sin comprometer la aplicación.

En general, recomendamos lo siguiente:

  1. Usa la menor cantidad posible de analizadores distintos. Así reducirás la superficie de «confusión» y la cantidad de posibles problemas de análisis.

  2. Usa un único punto de análisis en sistemas descentralizados. Si las solicitudes pasan una y otra vez entre distintos componentes del sistema, es muy probable que se usen diferentes analizadores (por ejemplo, debido a que hay servicios en distintos lenguajes). Para mitigar este problema, puedes analizar la URL una sola vez, en el punto de entrada de las solicitudes al sistema, y pasar la solicitud ya analizada. De este modo, se usa un solo analizador durante todo el recorrido de la solicitud.

  3. Comprende las diferencias entre los analizadores que usa tu lógica de negocio. Como a veces necesitamos analizar URL como parte de nuestro código o lógica de negocio, es importante que los desarrolladores que trabajan en la funcionalidad comprendan las variaciones entre los analizadores (como se indicó anteriormente).

Lee el informe técnico sobre la confusión de URL

Conoce más sobre los tipos de confusión de URL leyendo el informe técnico.