Skip to main content

Vulnerabilidades de confusão de URL em ação: explorando inconsistências entre parsers

Escrito por
Headshot of Snyk Security Research Team

Snyk Security Research Team

Claroty Team82

feature url confusion claroty

10 de janeiro de 2022

0 minutos de leitura

As URLs mudaram para sempre a forma como interagimos com computadores. Concebidas em 1992 e definidas em 1994, as URLs (Uniform Resource Locators, ou localizadores uniformes de recursos) continuam sendo um componente essencial da internet, permitindo que as pessoas naveguem pela web usando endereços descritivos e compreensíveis. Mas a necessidade de torná-las legíveis para humanos também criou a necessidade de dividi-las em componentes que as máquinas pudessem usar; essa tarefa é realizada por parsers de URL.

Considerando a onipresença das URLs há décadas, seria de se esperar que os parsers de URL usados hoje tivessem chegado a um consenso sobre como interpretar URLs semelhantes (ou até idênticas). Embora isso fosse ideal, está longe de ser verdade. Essa falta de consenso contribuiu para uma classe de vulnerabilidade chamada confusão de URL, que vamos explorar nesta publicação.

Observação: O processo de desenvolvimento dos protocolos da internet (HTTP, HTML, URL etc.) usa documentos conhecidos como Requests for Comments, ou RFCs. Vamos fazer referência frequente a esses documentos nesta publicação.

Em nossa pesquisa conjunta, analisamos diversas bibliotecas de parsing de URL em várias linguagens de programação e identificamos inconsistências na forma como cada uma dividia as URLs fornecidas em seus componentes básicos. Classificamos os tipos de inconsistência em quatro categorias e buscamos fluxos de código problemáticos tanto em aplicações web quanto em bibliotecas de código aberto. Esse trabalho revelou inúmeras vulnerabilidades, a maioria com uma destas causas principais:

  1. Há vários parsers de URL em uso

  2. Ao longo do tempo, foram publicados vários RFCs relacionados a URLs, e diferentes parsers implementaram RFCs diferentes

Nesta publicação, vamos conhecer a história das URLs, explorar possíveis causas de confusão entre parsers de URL, demonstrar uma prova de conceito de exploração e, em seguida, apresentar recomendações para proteger você contra ataques baseados em confusão de URL. Use o índice abaixo para ir direto à seção desejada:

  1. O que são URLs e como chegamos até aqui?

  2. Onde os parsers de URL se confundem?

  3. Prova de conceito de exploração (e CVEs atribuídos)

  4. Recomendações

A pesquisa realizada pelas equipes de pesquisa de segurança da Claroty e da Snyk foi inspirada em trabalhos anteriores de Orange Tsai, intitulados “A New Era of SSRF”, e em uma comparação entre WHATWG e RFC 3986 feita por Daniel Stenberg, criador do cURL. Agradecemos a eles por suas pesquisas inovadoras.

O que são URLs e como chegamos até aqui?

Quando pensamos em uma URL, imaginamos algo como https://snyk.io. Basicamente, ela indica para onde ir (my-site.com) e como chegar lá (por HTTPS). Além desses componentes, também encontramos outros elementos, como caminhos (https://my-site.com/about), símbolos de cerquilha seguidos por uma sequência de caracteres (fragmentos, por exemplo: https://my-site.com#contact) ou pontos de interrogação seguidos por parâmetros (consultas, por exemplo: https://my-site.com?source=li&device=mobile).

Generalizando e combinando os elementos acima, podemos dividir uma URL em string nos seguintes componentes principais:

scheme://authority/path?query#fragment

Por exemplo, uma URL pode ter esta aparência: https://example.com:8042/over/there?name=ferret#nose

Em nossa pesquisa, analisamos como vários parsers implementaram diferentes RFCs. Embora o formato de URL acima pareça relativamente simples, os RFCs que o definem mudaram bastante desde 1994. Essas adições e revisões levaram à forma atual da URL que conhecemos.

Linha do tempo dos padrões de URL, da RFC 1738, de 1994, à RFC 3986, de 2005, incluindo revisões para URLs relativas, URNs e IPv6.

Saiba mais: RFC 1738, RFC 1808, RFC 2141, RFC 2396, RFC 2732, RFC 3986

Para entender onde os parsers se confundem, primeiro precisamos analisar (rapidamente) o significado de cada componente da URL completa e como ele é definido.

Esquema

A seção de esquema define o protocolo (por exemplo, HTTP, HTTPS, FTP, Gopher etc.). Os RFCs especificam quais caracteres podem aparecer nesse componente e determinam que o esquema atual deve seguir o padrão ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ). Veja como isso funciona:

  • Primeiro caractere: a–Z

  • Demais caracteres: a–Z, 0–9, +, -, .

Isso significa que 1http não é um esquema válido (pois começa com um número), mas h2ttp é (pois o primeiro caractere precisa ser uma letra). Além disso, diferentemente dos outros componentes de uma URL, o esquema é o único obrigatório.

Autoridade (anteriormente Netloc)

Esse componente era chamado de “Netloc” (localização na rede) e passou a se chamar Autoridade. Ele é composto por três subcomponentes internos: informações do usuário, host e porta. De forma geral, uma autoridade tem esta estrutura: authority = [ userinfo "@" ] host [ ":" port ]. O subcomponente userinfo também pode ser dividido em seus componentes principais: username[:password]. Isso contempla URLs no formato scheme://user:password@domain:port/. Vale observar que, a partir da RFC-2396, o uso de user:password em texto simples passou a ser desaconselhado e foi descontinuado na RFC-3986.

Caminho

Esse componente indica quais recursos o solicitante quer acessar no servidor. Ele pode conter qualquer sequência de caracteres (exceto /,;,=,?), dividida em segmentos delimitados por /.

Embora pareça o componente mais simples, foi também o que mais mudou ao longo dos anos. A RFC-1738 e a RFC-1808 vinculavam a estrutura e as regras do componente de caminho ao componente de esquema. Depois, a RFC-2396 estabeleceu que cada segmento do caminho poderia usar ponto e vírgula (;) para definir parâmetros. Porém, pouco depois da RFC-2396, veio a RFC-3986, que descontinuou a sintaxe de parâmetros com ponto e vírgula.

Confuso? Pois os parsers também ficam!

Consulta

As consultas são pares de chave e valor transmitidos pela URL ao recurso solicitado. Os parâmetros da consulta vêm depois do primeiro ponto de interrogação (?) e terminam no sinal de cerquilha (#).

Em um componente de consulta, ponto e vírgula (;), barra (/), ponto de interrogação (?), dois-pontos (:), arroba (@), e comercial (&), sinal de igual (=), sinal de adição (+), vírgula (,) e cifrão ($) são caracteres reservados e serão codificados na URL quando usados.

Veja um exemplo de codificação de caracteres especiais em uma URL:

Fragmento

O fragmento é usado para identificar e acessar um segundo recurso dentro do primeiro recurso obtido, especificado pelo componente de caminho. Um componente de fragmento é indicado pela presença de uma cerquilha (#) e termina no fim da URI.

... e esses são todos os componentes!

URLs relativas

O último tópico desta seção são as referências relativas em URLs. Como as URLs são hierárquicas por natureza, uma URL pode ser relativa a outra. Por exemplo, dada uma URL “base” https://example.com/ e um segmento de string de URL iniciado pelo caminho, como /foo/bar, o parser vai resolvê-los como https://example.com/foo/bar. No entanto, primeiro é necessário fornecer a URL “base”.

Isso significa que os parsers precisam saber interpretar referências relativas. A RFC-3986 define três tipos de referências relativas:

  1. Referência de caminho de rede — começa com //, por exemplo, //example.com

  2. Referência de caminho absoluto — começa com /, por exemplo, /etc/passwd

  3. Referência de caminho relativo — não começa com /, por exemplo, foo/bar

Tipo de referência

Exemplo

Caminho de rede

//snyk.io

Caminho absoluto

/etc/passwd

Caminho relativo

app/login.js

Os dois últimos tipos precisam de uma URL base para serem resolvidos, enquanto a referência de caminho de rede precisa apenas que o esquema esteja presente. Portanto, se um parser usar HTTPS como esquema padrão, //example.com se transformará em https://example.com.

Onde os parsers de URL se confundem?

Depois de analisar todos os componentes de uma string de URL e revisar as mudanças nos RFCs ao longo dos anos, decidimos investigar parsers de URL e encontrar casos extremos que os levem a produzir resultados de parsing incorretos ou inesperados.

Durante nossa pesquisa, analisamos 15 bibliotecas (escritas em várias linguagens de programação), ferramentas para buscar conteúdo de URLs (por exemplo, curl e wget) e navegadores.

Encontramos muitas inconsistências entre os parsers e as classificamos em quatro categorias principais. Com as categorias descritas abaixo, podemos enganar a maioria dos parsers e criar vários comportamentos imprevisíveis, possibilitando uma ampla variedade de vulnerabilidades.

As categorias que definimos são:

  1. Confusão de esquema

  2. Confusão com barras

  3. Confusão com barras invertidas

  4. Confusão com codificação de URL

Sem mais delongas, vamos analisá-las!

Confusão de esquema

Quase todos os parsers de URL existentes ficam confusos quando o componente de esquema está ausente. A confusão surge porque a RFC 3986 determina que o esquema é a única parte obrigatória da URL, enquanto a RFC 2396 e as anteriores não fazem essa exigência. Implementar parsers que também tentem manter a compatibilidade com versões anteriores diante dessas nuances não é simples — e é daí que vem a confusão.

Para demonstrar isso, veja quatro bibliotecas Python diferentes às quais foi fornecida a URL google.com/abc:

Editor de código escuro comparando os resultados de análise da URL “google.com/abc” em urllib, urllib3, RFC 3986 e httptools

Como mostrado acima, a maioria dos parsers informa que o componente de host está vazio quando recebe a string google.com/abc. A Urllib3, porém, indica que o host é google.com e o caminho é /abc. Já a httptools afirma que essa URL é inválida. Em resumo, quase todos os parsers interpretam essa URL incorretamente, pois ela não segue as especificações dos RFCs.

Ainda assim, alguns parsers recorrem ao esquema padrão, como o curl neste caso:

Terminal exibindo uma solicitação curl para google.com e uma resposta HTML 301 Moved apontando para o endereço com www.

Nesse caso, invasores podem explorar a diferença entre os parsers para contornar a validação. Se um parser de URL tentar validar hosts específicos, mas não conseguir interpretar a URL corretamente para extrair o host, enquanto a biblioteca subjacente conseguir interpretar a URL corretamente (ou recorrer ao esquema padrão), o contorno acontece. Por exemplo:

Código Python que verifica uma URL em uma lista de bloqueio de endereços localhost antes de enviar uma solicitação GET para localhost/secret.txt

No exemplo acima, o urllib (mais especificamente, a função urlsplit) interpreta a URL como se não tivesse netloc, então a verificação é aprovada. Já o urllib3 recorre ao protocolo padrão http e busca o recurso proibido.

Confusão com barras

O próximo tipo de confusão envolve um número não padrão de barras na URL. A RFC 3986 determina que o componente de autoridade da URL deve vir depois de dois-pontos e duas barras, ://, e se estende até o fim da linha (EOL) ou até que um delimitador seja encontrado. Nesse caso, um delimitador pode ser uma barra (que indica o componente de caminho), um ponto de interrogação (componente de consulta) ou uma cerquilha (componente de fragmento).

Durante a pesquisa, observamos vários comportamentos dos parsers ao tentarem interpretar uma URL que não segue a sintaxe acima. Dada a URL http:///google.com, os parsers se comportaram de maneira interessante:

Saída do terminal que compara os resultados da análise da URL malformada http:///google.com em várias bibliotecas Python.

Como você pode ver, a maioria dos parsers afirmou que essa URL não tem host e interpretou /google.com como um caminho em uma URL sem host — o que é esperado de acordo com a RFC 3986. Conseguimos reproduzir esse comportamento com qualquer quantidade de barras depois do esquema. No entanto, encontramos um grupo de parsers que tentava “corrigir” a URL, ignorando barras extras ou ausentes (até certo ponto). Por exemplo, a função nativa fetch do JavaScript trata essas URLs como se estivessem corretas:

Console do navegador mostrando uma solicitação fetch para https://www.google.com e uma Promise resolvida contendo uma resposta HTML.

E isso também vale para curl:

Terminal exibindo comandos curl com URLs do Google malformadas e corrigidas, um erro de formato de URL e respostas 301 Moved.

Diferenças nos resultados de análise criam uma ampla superfície de ataque. Assim como na ideia de ataque anterior (confusão de esquema), e se fosse possível contornar as verificações porque o primeiro parser interpreta a URL de forma diferente do componente que busca o recurso? Veja este exemplo:

Editor de código escuro mostrando a análise de URL, uma verificação de lista de bloqueio para foo.com e um comando curl buscando dados

Mais uma vez, vemos uma asserção de netloc para um domínio bloqueado e, novamente, o netloc estará vazio devido à análise, então a verificação será aprovada. Mais adiante no código, o curl interpretará a URL de outra forma e tentará buscar o recurso inesperadamente. Isso pode levar a SSRFs e ao acesso a outros hosts não permitidos.

Confusão com barras invertidas

Uma variação da confusão com barras é a confusão com barras invertidas. Essa confusão pode ocorrer quando uma URL usa uma barra invertida (\) no lugar de uma barra (/), criando assim uma URL malformada. De acordo com a RFC 396, uma barra invertida é diferente de uma barra e deve ser interpretada de outra forma, fazendo com que http://google.com seja diferente de http:\\google.com. Seguindo a RFC, a maioria dos parsers de URL programáticos realmente interpreta essas duas URLs de maneira diferente:

Saída do terminal comparando como bibliotecas de análise de URLs do Python interpretam a URL malformada http:\\google.com

O Chrome, porém, opta por interpretar a barra invertida como se fosse uma barra:

Trecho de código que mostra uma resposta HTTP 302 Found redirecionando para https://www.google.com

Ele acessará a URL como se ela fosse válida. Levando isso ao extremo, esse comportamento também ocorre com https:/\google.com, e o Chrome disponibilizará o recurso, como (in)esperado.

Confusão com codificação de URL

A última categoria de confusão é a confusão com codificação de URL. Ela ocorre quando uma URL contém um trecho codificado que não deveria estar ali.

Em geral, a codificação de URL é uma forma de incluir caracteres não imprimíveis nas URLs. Isso é feito usando o valor hexadecimal do caractere, precedido pelo símbolo %. Assim, um g é representado como %67 quando codificado em URL. Esse método permite que as URLs permaneçam totalmente textuais e legíveis, independentemente dos caracteres usados. Embora seja voltado a caracteres não imprimíveis, também é possível codificar caracteres imprimíveis em URLs — e é aí que surge a confusão.

A RFC 3986 estabelece que todos os componentes de uma URL, exceto o esquema, podem ser codificados. Na prática, porém, muitos parsers não analisam o componente netloc.

Estes são os resultados da análise quando fornecemos aos parsers a URL http://google.com na versão codificada:

Captura de tela do terminal comparando resultados da análise de URLs para uma URL HTTP com caracteres codificados por porcentagem, incluindo um erro de URL inválida.

Embora os resultados acima pareçam esperados, tanto o urllib quanto o requests do Python demonstraram um comportamento interessante ao receber uma URL codificada:

Exemplos de código usando requests e urllib do Python para acessar 127.0.0.1 com um endereço de loopback codificado.

Nos dois casos acima, uma solicitação foi enviada inesperadamente para 127.0.0.1.

Diante disso, essa discrepância cria mais uma superfície de ataque, já que padrões simples de Regex não identificam essas strings e, assim, talvez seja possível contornar as verificações novamente.

Prova de conceito da exploração (e CVEs emitidos)

Agora que entendemos onde está a confusão, vamos ver como explorar esses problemas e as falhas que encontramos. Vamos detalhar uma CVE, mas a lista completa de CVEs está abaixo.

Clearance (Ruby): CVE-2021-23435: vulnerabilidade de redirecionamento aberto

Vulnerabilidades de redirecionamento aberto ocorrem quando um aplicativo web aceita uma entrada controlada pelo usuário que especifica a URL para a qual ele será redirecionado após uma ação, como o login. Para visualizar melhor o ataque, veja este diagrama:

Diagrama mostrando um usuário sendo redirecionado de foo.com para evil.com, com servidores identificados como foo.com e evil.com.

Como você pode ver, o invasor fornece à vítima uma URL para que ela a acesse. Com a configuração correta, o servidor redirecionará o usuário para um site controlado pelo invasor.

Clearance é uma gem do Ruby que aprimora o mecanismo de autenticação do framework Rails adicionando autenticação por email-and-password. Após o login ou logout, ela redireciona o usuário usando uma URL obtida de uma solicitação anterior (ou seja, o recurso solicitado antes da página de login).

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

O código vulnerável está dentro da função return_to (o callback executado após o login/logout):

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

Embora a função return_to tenha sido criada pensando em redirecionamentos abertos, ela não permite que os usuários forneçam livremente uma URL return_to. No entanto, se um usuário solicitar um recurso auth-required sem estar conectado, o sistema chamará store_location e redirecionará o navegador para a página de login. Portanto, se um invasor conseguir convencer uma vítima a clicar em um link como http://target.com/////evil.com, a vulnerabilidade será acionada.

Por que ocorre um redirecionamento aberto? Por causa de vários parsers!

Como vimos, store_location armazena o caminho completo da URL (isso também acontece porque o Ruby ignora várias barras consecutivas em URLs). Isso significa que ele armazena /////evil.com no cache. Quando URI.parse é chamado, ele remove duas barras extras, resultando em ///evil.com. Quando um navegador, como o Chrome, recebe essa URL, ele a trata como uma referência de caminho de rede e redireciona o cliente para http://evil.com.

Hoje, os navegadores tendem a “perdoar” esses erros em URLs e tentar corrigi-los porque, ao longo dos anos, precisaram lidar com muitas URLs imperfeitas ou incompatíveis com a RFC. Para serem resilientes a erros de clientes e desenvolvedores, eles passaram a omitir ou adicionar barras em erros comuns.

Este é um trecho do código-fonte do projeto 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.

Outras vulnerabilidades

Veja outras vulnerabilidades causadas por diferenças entre parsers:

  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)

Recomendações para evitar confusão com URLs

Como os problemas discutidos nesta publicação são causados pelo uso de vários parsers e pela forma como eles funcionam, é importante saber quais parsers estão presentes no seu aplicativo ao construí-lo. Isso se torna mais complexo nas arquiteturas atuais, como microsserviços e malhas de serviços, e pode levar algum tempo para entender por completo quais componentes fazem a análise no seu aplicativo e como uma solicitação percorre o sistema.

Depois de mapear os parsers, é essencial que os desenvolvedores entendam bem as diferenças entre a lógica de análise de cada um, para que possam trabalhar com eficiência sem comprometer o aplicativo.

Em geral, recomendamos o seguinte:

  1. Use o menor número possível de parsers diferentes. Assim, você reduz a superfície de “confusão” e diminui a quantidade de problemas de análise possíveis.

  2. Use um único ponto de análise em sistemas descentralizados. Se uma solicitação passar repetidamente por vários componentes do sistema, há uma boa chance de diferentes parsers serem acionados, por exemplo, devido a serviços desenvolvidos em linguagens diferentes. Para reduzir esse risco, você pode analisar a URL uma única vez no ponto de entrada das solicitações e encaminhar a solicitação já analisada. Dessa forma, apenas um parser será usado durante todo o percurso da solicitação.

  3. Entenda as diferenças entre os parsers usados pela lógica de negócios. Como às vezes precisamos analisar URLs como parte do código ou da lógica de negócios, é importante que os desenvolvedores que trabalham no recurso entendam as variações entre os parsers, conforme explicado acima.

Leia o white paper sobre confusão de URLs

Entenda melhor os diferentes tipos de confusão de URLs lendo o white paper.