SSRF (Server-Side Request Forgery) acontece quando uma aplicação busca um recurso a partir de uma URL controlada pelo usuário sem validar adequadamente o destino. O atacante não ataca o servidor diretamente — ele faz o servidor atacar por ele. Como a requisição parte de dentro da infraestrutura, ela frequentemente atravessa firewalls, alcança serviços internos que não deveriam ser acessíveis e carrega a confiança da rede interna.
Em ambientes de nuvem, o SSRF é especialmente perigoso: ele é a porta de entrada clássica para o roubo de credenciais temporárias via endpoint de metadados.
Onde o SSRF nasce
Qualquer funcionalidade que aceite uma URL e a busque no backend é candidata:
- “Importe sua foto de perfil a partir de uma URL”
- Webhooks e integrações (“envie eventos para esta URL”)
- Geradores de PDF/screenshot que renderizam uma página
- Validadores de URL, link previews, proxies de imagem
- Parsers de XML com entidades externas (XXE que vira SSRF)
# Endpoint vulnerável: busca a URL que o usuário mandar, sem restrição
@app.post("/importar")
def importar():
url = request.json["url"]
resp = requests.get(url, timeout=5) # <- SSRF
return resp.content
O alvo mais valioso: metadados de nuvem
Provedores de nuvem expõem um endpoint de metadados em um IP link-local acessível apenas de dentro da instância. Em muitos ambientes ele entrega credenciais temporárias da role associada à máquina:
http://169.254.169.254/latest/meta-data/iam/security-credentials/
Se o servidor faz a requisição que o atacante dita, ele pode pedir esse endereço e receber de volta AccessKeyId, SecretAccessKey e Token — credenciais válidas para falar com a API da nuvem como se fosse a aplicação. É a escalada que transforma um “busque esta URL” em comprometimento de conta inteira.
Mitigação específica: exija o IMDSv2 (fluxo baseado em token com
PUT+ cabeçalho e TTL de hop = 1). Ele quebra a maioria dos SSRFs ingênuos, que só conseguem fazer um GET simples. Mas trate isso como uma camada, não como solução única.
Outros alvos internos comuns: bancos de dados e caches sem autenticação (http://127.0.0.1:6379 em Redis), painéis administrativos internos, APIs de orquestração (Kubernetes, Consul) e o velho localhost.
SSRF cego (blind)
Nem sempre a resposta volta para o atacante. No SSRF cego, a aplicação faz a requisição mas não devolve o corpo. Ainda assim ele é explorável:
- Exfiltração por canal lateral: diferenças de tempo de resposta ou de código de status revelam se uma porta interna está aberta (varredura de rede interna).
- Interações fora de banda: apontar para um domínio controlado (servidor DNS/HTTP do atacante) e observar a chamada chegar confirma o SSRF e pode vazar dados via subdomínio.
Por que filtros ingênuos falham — os bypasses
A reação instintiva é “bloquear localhost e 169.254.169.254”. Isso quase nunca é suficiente. Os bypasses clássicos — você gera todas estas variações de um IP automaticamente no Helper SSRF:
Representações alternativas do IP:
http://2130706433/ # 127.0.0.1 em decimal
http://0x7f000001/ # 127.0.0.1 em hexadecimal
http://0177.0.0.1/ # octal
http://127.1/ # forma curta
http://[::1]/ # IPv6 loopback
http://[::ffff:169.254.169.254]/ # IPv4 mapeado em IPv6
Truques de URL e parsing:
http://169.254.169.254.nip.io/ # DNS que resolve para o IP interno
http://atacante.com@169.254.169.254/ # confunde validadores que olham só o "host" textual
http://metadata/ # caracteres unicode/encoding
Redirect: o atacante aponta para http://site-do-atacante.com/r, que responde 302 Location: http://169.254.169.254/.... Se a aplicação segue redirects e só validou a URL inicial, o filtro é contornado.
DNS rebinding: o domínio do atacante resolve para um IP público na primeira consulta (passando na validação) e para 169.254.169.254 na segunda consulta (no momento do fetch real). A janela entre validar e buscar é a vulnerabilidade. É o motivo pelo qual validar a string da URL não basta: o que importa é o IP de destino no instante da conexão.
A arquitetura de defesa correta
SSRF não se resolve com uma regex. A defesa é em camadas, do código à rede.
1. Allowlist, nunca blocklist
Defina explicitamente os destinos permitidos. Se a funcionalidade só precisa falar com api.parceiro.com, só permita esse host. Tudo o mais é negado por padrão.
ALLOWED_HOSTS = {"api.parceiro.com", "cdn.parceiro.com"}
def fetch_seguro(url):
parts = urlparse(url)
if parts.scheme not in ("https",): # só https
raise ValueError("esquema não permitido")
if parts.hostname not in ALLOWED_HOSTS: # allowlist de host
raise ValueError("host não permitido")
# Resolve e valida o IP ANTES de conectar (ver passo 2)
...
2. Resolva o DNS e valide o IP — e conecte nesse IP
Para derrotar DNS rebinding e redirects, resolva o nome, rejeite IPs privados/reservados e force a conexão a usar exatamente o IP validado (sem nova resolução):
import ipaddress, socket
BLOCKED_NETS = [
ipaddress.ip_network(n) for n in [
"127.0.0.0/8", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
"169.254.0.0/16", "::1/128", "fc00::/7", "fe80::/10",
]
]
def ip_permitido(host):
for fam, _, _, _, sockaddr in socket.getaddrinfo(host, None):
ip = ipaddress.ip_address(sockaddr[0])
if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved:
return False
if any(ip in net for net in BLOCKED_NETS):
return False
return True
Valide todos os IPs retornados (um host pode ter vários registros A/AAAA) e revalide após cada redirect — ou, mais simples, desabilite o follow de redirects no cliente HTTP.
3. Segmentação de rede (a camada que salva)
Trate o código como falível e isole por rede: a carga de trabalho que faz fetch externo não deveria ter rota para o endpoint de metadados, para a sub-rede de bancos de dados ou para painéis internos. Egress filtering e security groups restritivos transformam um SSRF bem-sucedido em um beco sem saída.
4. Endurecimento específico de nuvem
- IMDSv2 obrigatório com
hop limit = 1. - Roles com privilégio mínimo — se as credenciais vazarem, o estrago é contido.
- Bloquear, por rota de rede, o acesso da aplicação ao
169.254.169.254quando ela não precisa de metadados.
Como testamos SSRF
- Catalogar toda entrada que vira uma requisição no servidor (URLs, webhooks, parsers, importadores).
- Apontar para um listener fora de banda controlado e confirmar a chamada chegando (prova de SSRF, inclusive cego).
- Testar a bateria de bypasses: IP decimal/hex/octal, IPv6,
@,nip.io, redirect 302, DNS rebinding. - Tentar alcançar
169.254.169.254,127.0.0.1em portas internas e serviços de orquestração. - Medir o impacto real: leitura de metadados, varredura interna, acesso a serviços sem autenticação.
Checklist de mitigação
- Allowlist de host e esquema (
https) — negar por padrão. - Resolver DNS, rejeitar IPs privados/reservados e conectar no IP validado.
- Não seguir redirects (ou revalidar o destino a cada salto).
- Segmentação de rede / egress filtering isolando a carga do metadado e da rede interna.
- IMDSv2 obrigatório, roles com privilégio mínimo.
- Desabilitar esquemas perigosos (
file://,gopher://,dict://) no cliente HTTP. - Timeouts curtos e resposta genérica (não devolver corpo/erro que sirva de oráculo).
SSRF é a vulnerabilidade que melhor ilustra por que “validar a entrada” e “validar o destino” são problemas diferentes. A defesa robusta nunca confia na string da URL — ela controla para qual IP, em qual rede, o servidor tem permissão de conectar.