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://meta​data/             # 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.254 quando ela não precisa de metadados.

Como testamos SSRF

  1. Catalogar toda entrada que vira uma requisição no servidor (URLs, webhooks, parsers, importadores).
  2. Apontar para um listener fora de banda controlado e confirmar a chamada chegando (prova de SSRF, inclusive cego).
  3. Testar a bateria de bypasses: IP decimal/hex/octal, IPv6, @, nip.io, redirect 302, DNS rebinding.
  4. Tentar alcançar 169.254.169.254, 127.0.0.1 em portas internas e serviços de orquestração.
  5. 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.