A Lista de Permissões que Não Permite: Teste de Substring em uma Ferramenta de Agente LLM
O desafio
Atlas é o assistente que a Halcyon mantém para o próprio time. Ele tem exatamente uma ferramenta que toca a rede: fetch_url, para ler uma página que um colega envia antes de responder sobre ela. O time sabia que dar um cliente HTTP a um modelo é o caminho curto para server-side request forgery, então escreveu uma proteção antes de publicar. Apenas três domínios de documentação são alcançáveis. Apenas http e https. Uma lista de bloqueio cobre o endereço de metadados da nuvem e o loopback. Passou por revisão duas vezes. Mesmo assim não segura. Uma das três verificações aceita hosts que ninguém na Halcyon possui, e quem conseguir colocar um link na frente do Atlas pode apontar a ferramenta para uma infraestrutura que controla. Leia os três arquivos e diga o nome da função cuja verificação deixa passar hosts controlados pelo atacante.
O que você vai aprender
- Diferenciar um teste de pertinência de um teste de substring em Python
- Explicar por que a comparação por substring é segura em uma lista de bloqueio e insegura em uma lista de permissões
- Construir um hostname que engana uma lista de permissões baseada em substring
- Descrever como uma ferramenta de agente transforma um SSRF em saída legível para o atacante
- Escrever uma comparação de host que não pode ser ampliada por um sufixo controlado pelo atacante
Habilidades testadas
Pré-requisitos
- Leitura de funções e comprehensions em Python
- O que são um nome de host e um subdomínio
Como funciona
Dar um cliente HTTP a um modelo é dar a ele um forjador de requisições. O modelo decide a URL, o servidor faz a requisição, e o servidor fica dentro da sua rede. Então toda a propriedade de segurança de uma ferramenta fetch_url se resume a uma pergunta: esse host é um dos que escolhemos permitir?
O Atlas responde a essa pergunta em host_allowed, e por acidente responde a uma pergunta diferente. any(domain in host for domain in ALLOWED_DOMAINS) parece um teste de pertinência, e em scheme_allowed a mesma palavra-chave realmente é um, porque urlparse(url).scheme in ("http", "https") compara contra os elementos de uma tupla. Entre duas strings, in significa substring. host_allowed está perguntando se o texto docs.halcyon.example aparece em algum lugar dentro do host.
Um atacante que possui qualquer domínio possui um número infinito de hosts que contêm esse texto. docs.halcyon.example.attacker.example é um subdomínio comum de attacker.example. Não custa nada, resolve para onde quer que ele aponte, e satisfaz a verificação. Esconder o nome permitido no meio funciona igualmente bem.
A lista de bloqueio algumas linhas acima também é um teste de substring, e esse não é um bug. Ampliar uma lista de bloqueio faz com que ela capture mais coisas, então any(bad in url.lower() for bad in BLOCKED) recusa mais URLs do que o pretendido, em vez de menos. A direção importa: uma comparação frouxa é perigosa quando concede acesso e apenas tosca quando nega.
O que torna isso pior do que um SSRF cego é que o agente relata de volta. O corpo da resposta é anexado à conversa como saída de ferramenta e depois resumido para quem estiver falando com o Atlas, então o atacante não precisa de um canal lateral para ler o que o servidor buscou.
Erros comuns
- Responder com scheme_allowed. Usa a mesma palavra-chave, mas compara contra uma tupla, então é um teste de pertinência de verdade e está correto.
- Responder com target_allowed. Sua ordem e cobertura estão corretas; ela falha apenas porque o auxiliar em que confia está errado.
- Chamar a lista de bloqueio de bug. A comparação por substring em uma lista de bloqueio bloqueia demais, que é a direção segura.
- Culpar allow_redirects. Está explicitamente definido como False, que é a única coisa que os autores acertaram pelo motivo certo.
- Achar que o endereço de metadados é necessário. Qualquer host do atacante já é suficiente, porque o corpo buscado volta para o modelo.
Como se proteger
Compare hosts da mesma forma que o DNS: como rótulos inteiros, por igualdade.
- Faça o parse uma vez com
urlparse, usehostnameem vez denetlocpara que credenciais e portas já venham removidas, converta para minúsculas e remova um ponto final. - Teste contra um
setcom==ouinnesse conjunto. Se subdomínios precisarem ser permitidos, verifiquehost == d or host.endswith("." + d), nunca uma substring simples. - Resolva o nome de host e rejeite a requisição se o endereço for privado, loopback, link-local ou multicast, depois conecte-se ao endereço que você validou, para que o DNS não possa mudar debaixo de você.
- Mantenha os redirecionamentos desativados, ou execute a verificação completa novamente a cada salto.
- Filtre a saída (egress) do container em que o agente roda. A lista de permissões é o primeiro controle, não o único.
Solução completa
Estatísticas da comunidade
Hacks de hoje relacionados
Colaboradores creditados
Estes hackers relataram problemas, melhoraram o conteúdo e ajudaram a fortalecer esta página.