Blog

SSRF: o perigo que quase ninguém testa.

Existe uma funcionalidade inofensiva em quase todo produto: buscar alguma coisa numa URL que o usuário informou.

Importar imagem de perfil por link. Validar um webhook que o cliente cadastrou. Gerar prévia de um site. Baixar um arquivo de uma integração.

Todas fazem a mesma coisa: a sua aplicação faz uma requisição pra um endereço escolhido por outra pessoa. E a sua aplicação está dentro da rede, onde os serviços internos costumam não exigir autenticação nenhuma porque “só a gente acessa”.

Isso é SSRF, e é a falha que transforma um recurso de conveniência num caminho pro seu ambiente interno.

O que dá pra alcançar

O alvo clássico é o serviço de metadados da nuvem, que responde num endereço fixo e entrega credencial temporária da instância. Um pedido de “gere a prévia dessa URL” apontando pra lá pode devolver chave de acesso.

Depois vêm os endereços internos: painel de administração de banco, cache sem senha, fila, serviço interno que confia em quem está na rede, e localhost da própria aplicação, que às vezes expõe rotas de depuração.

E file:///etc/passwd, se a biblioteca aceitar esquemas além de HTTP.

O detalhe que assusta: nada disso exige credencial. O atacante está usando a sua aplicação como procurador autenticado dentro da sua rede.

Por que a validação óbvia não funciona

A primeira tentativa é sempre bloquear endereços privados na string:

if (url.includes("127.0.0.1") || url.includes("localhost")) throw new Error();

As formas de contornar são muitas e conhecidas: notação decimal do endereço, IPv6, redirecionamento, domínio que resolve pra endereço interno, letras maiúsculas, credenciais na URL pra confundir o parser.

E a mais difícil de todas: o domínio resolve pra um endereço público na hora da validação e pra um endereço interno na hora da requisição. Isso tem nome, e a janela entre validar e conectar é o que torna qualquer validação por nome insuficiente.

O que funciona

Resolver o nome você mesmo, validar o IP, e conectar naquele IP. Isso fecha a janela entre validação e uso.

import { lookup } from "node:dns/promises";
import { isIP } from "node:net";

const BLOCKED = [
  /^127\./, /^10\./, /^192\.168\./, /^169\.254\./,
  /^172\.(1[6-9]|2\d|3[01])\./, /^0\./, /^::1$/, /^fe80:/i, /^fc00:/i,
];

async function resolveSafely(rawUrl: string) {
  const url = new URL(rawUrl);
  if (!["http:", "https:"].includes(url.protocol)) throw new BadRequestException();

  const { address } = await lookup(url.hostname);
  if (BLOCKED.some((re) => re.test(address))) throw new BadRequestException();

  return { url, address };
}

E na hora de conectar, use o endereço resolvido com o cabeçalho Host correto, pra não resolver de novo.

Não seguir redirecionamento automaticamente. Ou, se precisar, validar cada salto com a mesma regra. Redirecionamento é o contorno mais usado, porque a primeira URL passa na validação e a segunda é a que interessa.

const res = await fetch(url, { redirect: "manual" });

Lista de permissão, quando possível. Se o produto só precisa buscar de domínios conhecidos, a lista de permissão resolve o problema inteiro e todas as tentativas de contorno morrem. Vale sempre perguntar se o requisito é mesmo “qualquer URL”.

A defesa que eu confio mais

Todas as anteriores são código, e código tem exceção esquecida.

A defesa estrutural é de rede: o serviço que faz requisições externas roda numa sub-rede sem rota pros serviços internos, saindo por um proxy de egresso que só permite destinos públicos.

Aí não importa quantos contornos de parser existam. O pacote não tem por onde chegar.

Some a isso desabilitar o acesso ao serviço de metadados da nuvem a partir dos contêineres que não precisam dele, ou exigir a versão do protocolo que requer token, que quebra a exploração ingênua.

Onde procurar no seu sistema

Uma varredura curta que costuma render:

grep -rn "fetch(\|axios.get(\|httpClient" src/ | grep -i "req\.\|dto\.\|input\|url"

Os candidatos típicos: importação de imagem por URL, cadastro de webhook, prévia de link, importação de feed, integração configurável pelo cliente e conversão de documento por URL.

Vale incluir na lista o que gera PDF a partir de HTML: se o HTML vem do usuário e o renderizador busca recursos, você tem SSRF pelo caminho indireto.

O teste

Cadastre um webhook apontando pro endereço do serviço de metadados da nuvem e veja o que volta.

Se voltar JSON com credencial, você tem um achado grave. Se der erro de conexão, teste também um domínio público que você controla e que redireciona pra um endereço interno, porque esse é o caso que a validação ingênua deixa passar.

Dez minutos, e é o tipo de teste que raramente é feito porque a funcionalidade parece inofensiva demais pra ser perigosa.

Sobre a família de problemas em que isso se encaixa, escrevi em como construir aplicações seguras.

← Voltar pro blog