Você monta uma ferramenta útil: o visitante digita o endereço do site dele, o seu servidor busca a página e devolve um relatório. Simples, e é exatamente onde mora a falha chamada SSRF — Server-Side Request Forgery, ou requisição forjada do lado do servidor.
O raciocínio do atacante é curto: o servidor faz requisições em nome dele, e o servidor está dentro da rede. Coisas que o navegador dele jamais alcançaria, o seu servidor alcança.
O que dá para pedir
# o proprio servidor
http://127.0.0.1/
http://localhost/phpmyadmin/
# a rede interna da hospedagem
http://192.168.0.1/
http://10.0.0.5:3306/
# metadados de nuvem: aqui costumam vir credenciais
http://169.254.169.254/latest/meta-data/
# nem sempre e HTTP
file:///etc/passwd
gopher://interno:6379/_SET%20chave%20valor
O último merece atenção: gopher:// permite montar tráfego arbitrário e, em servidores mal configurados, falar com Redis, memcached ou SMTP internos. Por isso restringir o esquema não é detalhe.
Por que a validação óbvia não resolve
Não resolve
if (str_contains($url, '127.0.0.1')) {
recusa();
}
Resolve
$ips = dns($host);
foreach ($ips as $ip) {
if (faixa_privada($ip)) recusa();
}
Bloquear por texto é inútil porque o mesmo endereço tem infinitas grafias: 127.1, 0177.0.0.1, 2130706433, [::1], ou simplesmente um domínio público que aponta para 127.0.0.1 no DNS. O texto muda; o IP de destino, não. Valide o IP resolvido, nunca a string.
A parte que quase todo mundo esquece
Suponha que você validou direito. O visitante informa exemplo.com, o DNS resolve para um IP público, tudo certo — e o servidor busca a página. Só que exemplo.com responde:
HTTP/1.1 302 Found
Location: http://169.254.169.254/latest/meta-data/
Se você usou CURLOPT_FOLLOWLOCATION, o cURL segue o redirecionamento sozinho, sem passar pela sua validação de novo. Toda a defesa foi contornada por três linhas de resposta HTTP.
A regra: siga redirecionamento manualmente e revalide o endereço a cada salto. Não existe atalho aqui — FOLLOWLOCATION e defesa contra SSRF são incompatíveis.
O conjunto mínimo
- Esquema — só
httpehttps. Nada defile,gopher,dict,ftp. - Porta — só 80 e 443. Porta livre é convite para falar com serviço interno.
- Sem credenciais na URL —
http://usuario:senha@host/confunde parsers e às vezes o servidor. - Resolva o nome e cheque todos os IPs — A e AAAA. Um único apontando para dentro já basta para recusar.
- Faixas proibidas — privadas, reservadas, loopback e link-local. Em PHP,
FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGEcobre quase tudo. - Redirecionamento manual, com limite de saltos e revalidação em cada um.
- Timeout curto e limite de tamanho — senão a ferramenta vira porta de negação de serviço contra você mesmo.
Uma janela que continua aberta
Entre resolver o DNS e fazer a requisição existe um intervalo. Um atacante com controle do próprio DNS pode responder com IP público na primeira consulta e IP interno na segunda — é o ataque conhecido como DNS rebinding.
Fechar isso exige resolver uma vez e forçar a conexão naquele IP específico, em vez de deixar o cliente HTTP resolver de novo. Dá para fazer com CURLOPT_RESOLVE. Vale a pena quando a ferramenta é crítica; para um diagnóstico público com timeout curto e sem retorno do conteúdo bruto, o risco residual é pequeno — mas é honesto saber que ele existe, e não fingir que a defesa é completa.
Como saber se você tem esse problema
Procure no código qualquer lugar onde uma URL vinda do usuário chega em file_get_contents, curl_exec, fopen ou biblioteca de requisição. Casos comuns que ninguém enxerga como risco: importar imagem por URL, webhook configurável pelo cliente, pré-visualização de link, validador de feed, integração que aceita endpoint do próprio usuário.
Se existir um desses e não existir validação de IP resolvido, você tem a falha. E ela é das que aparecem em varredura automatizada.