Prevenir SSRF no Node.js é essencial quando a aplicação acessa URLs fornecidas direta ou indiretamente pelo usuário. SSRF, ou Server-Side Request Forgery, permite que um atacante faça o servidor enviar requisições para destinos internos, serviços de metadata da cloud, painéis administrativos ou portas que não deveriam estar expostas.
O risco aparece em importadores de imagem, webhooks de teste, previews de links, proxies, leitores de feed, conversores de PDF e integrações que aceitam uma URL. Validar apenas o texto inicial não é suficiente, porque DNS, redirects, IPv6, URLs codificadas e respostas intermediárias podem levar a outro destino.
Neste guia, você aprenderá a usar allowlists, validar protocolos e portas, resolver DNS, bloquear redes privadas, controlar redirects, proteger metadata, limitar respostas e testar tentativas de bypass.
O que é SSRF?
SSRF ocorre quando um atacante influencia uma requisição feita pelo servidor. A referência SSRF da OWASP descreve o ataque. A SSRF Prevention Cheat Sheet reúne defesas.
Exemplo vulnerável
app.get('/preview', async (req, res) => {
const response = await fetch(req.query.url);
res.send(await response.text());
});O cliente pode tentar destinos como localhost, serviços internos ou metadata da plataforma.
Impactos
- leitura de serviços internos;
- acesso a metadata e credenciais temporárias;
- port scanning interno;
- contorno de firewall;
- chamadas a painéis administrativos;
- exfiltração por redirects;
- DoS com respostas grandes ou lentas.
Melhor defesa: allowlist
Quando possível, aceite apenas hosts conhecidos:
const allowedHosts = new Set([
'images.example-cdn.com',
'api.partner.example'
]);
function validateHost(url) {
if (!allowedHosts.has(url.hostname)) {
throw new Error('Host não permitido');
}
}Allowlist é mais segura que tentar bloquear todos os destinos perigosos.
Valide o protocolo
if (!['https:'].includes(url.protocol)) {
throw new Error('Protocolo não permitido');
}Não permita file:, ftp:, gopher: ou protocolos desconhecidos. Mesmo HTTP pode ser proibido quando HTTPS é obrigatório.
Valide a porta
const port = url.port || '443';
if (port !== '443') {
throw new Error('Porta não permitida');
}Uma URL HTTPS em porta arbitrária ainda pode alcançar serviços internos.
Credenciais na URL
if (url.username || url.password) {
throw new Error('Credenciais na URL não são permitidas');
}Evite URLs como https://user:pass@host, que podem confundir validações e vazar dados.
Normalização com URL
let parsed;
try {
parsed = new URL(input);
} catch {
throw new ValidationError('URL inválida');
}Não valide com regex simples. A classe URL ajuda a interpretar hostname, protocolo e porta de forma consistente.
Localhost
Bloqueie nomes e endereços de loopback:
localhost
127.0.0.0/8
::1Não basta bloquear a string 127.0.0.1. Existem representações alternativas e IPv6.
Redes privadas
Bloqueie faixas privadas e especiais:
- 10.0.0.0/8;
- 172.16.0.0/12;
- 192.168.0.0/16;
- 169.254.0.0/16;
- 100.64.0.0/10;
- faixas IPv6 locais e link-local;
- multicast e endereços reservados.
Resolução DNS
import dns from 'node:dns/promises';
const addresses = await dns.lookup(hostname, {
all: true,
verbatim: true
});Valide todos os endereços retornados. Um hostname pode resolver para IPv4 e IPv6.
DNS rebinding
Um domínio pode resolver primeiro para IP público e depois para IP privado. Validar uma resolução e deixar o cliente HTTP resolver novamente cria uma janela de ataque.
Fixando o endereço validado
Uma defesa avançada resolve o hostname, valida o IP e conecta exatamente ao endereço aprovado, mantendo o hostname para SNI e Host quando necessário. Use uma biblioteca ou agente que permita lookup controlado.
const agent = new Agent({
connect: {
lookup: safeLookup
}
});Teste cuidadosamente TLS, redirects e IPv6.
Redirects
Um destino público pode redirecionar para localhost. Desative redirects automáticos ou valide cada etapa:
const response = await fetch(url, {
redirect: 'manual'
});Validando cada redirect
- Receba o header Location.
- Resolva contra a URL atual.
- Valide protocolo, host, porta e IP.
- Limite a quantidade.
- Faça a próxima requisição.
Defina no máximo poucos redirects.
Metadata da cloud
Serviços de metadata costumam usar endereços link-local. Bloqueie a rede no código e na infraestrutura. Quando disponível, exija versões do serviço com token e desabilite acesso desnecessário.
Defesa de rede
O processo da aplicação não deve ter acesso irrestrito a toda a rede interna. Use egress firewall, network policies, service mesh ou proxy de saída.
Proxy de saída
Um egress proxy central pode aplicar allowlists, DNS seguro, logs e limites. A aplicação envia requisições externas apenas por esse componente.
Timeout
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 3000);
try {
return await fetch(url, { signal: controller.signal });
} finally {
clearTimeout(timer);
}Timeout reduz conexões presas, mas não corrige SSRF.
Limite de resposta
Não use response.text() sem limite em conteúdo arbitrário. Leia o stream e interrompa após o máximo permitido.
const MAX_BYTES = 2 * 1024 * 1024;Content-Length
O header ajuda, mas pode estar ausente ou incorreto. Continue contando bytes durante a leitura.
Tipos de conteúdo
Se o recurso deve ser imagem, valide Content-Type e o formato real. Não confie apenas na extensão.
Compressão
Uma resposta pequena comprimida pode expandir enormemente. Defina limite após descompressão e proteja contra compression bombs.
Upload de imagem por URL
Baixe em um worker isolado, limite tamanho, dimensões e tempo, depois reencode a imagem. Não sirva o conteúdo bruto como HTML.
Preview de links
Extraia apenas metadados necessários. Não execute scripts, não carregue recursos secundários e não envie cookies da aplicação.
Webhooks de teste
Se o usuário configura um endpoint, considere permitir apenas HTTPS público e executar testes por uma infraestrutura de egress dedicada.
Headers
Não encaminhe Authorization, cookies, headers internos ou trace baggage para um destino controlado pelo usuário.
const headers = {
'user-agent': 'PreviewBot/1.0',
accept: 'text/html,application/xhtml+xml'
};Host header
Não permita que o cliente escolha um Host diferente do destino validado. Isso pode alcançar virtual hosts internos.
Autenticação em URLs externas
Quando uma integração precisa de token, associe a credencial a um host fixo. Nunca envie um segredo para uma URL arbitrária.
Consulte Gestão de Segredos no Node.js.
Proteção contra redirects com segredo
Não encaminhe Authorization ao mudar de origin. Reavalie cada redirect e remova headers sensíveis.
URL assinada
URLs pré-assinadas podem conter credenciais na query. Não registre o valor e aceite apenas provedores e hosts esperados.
SSRF cego
Mesmo sem devolver a resposta, o atacante pode observar tempo, status indireto ou efeitos no serviço interno. Não considere seguro apenas porque o corpo não é mostrado.
Port scanning
Diferenças de timeout e erro revelam portas abertas. Padronize respostas e aplique allowlist.
Erros para o cliente
{
"code": "URL_NOT_ALLOWED",
"message": "Não foi possível acessar o endereço informado"
}Não revele IP resolvido, porta interna ou motivo detalhado.
Logs
Registre host normalizado, categoria do bloqueio, duração e request ID. Não registre query completa quando pode conter tokens.
Veja Logs com Pino no Node.js.
Auditoria
Alterações de allowlist, tentativas repetidas contra rede privada e acessos a metadata devem ser auditados. Consulte Logs de Auditoria no Node.js.
Rate limiting
Limite requisições de preview, importação e teste de webhook. SSRF também pode ser usado para DoS.
Consulte Rate Limiting no Node.js.
Filas
Downloads externos podem ser executados em workers com concorrência e rede restritas. O job deve carregar apenas a URL e o tenant necessários.
Multi-tenancy
Allowlists específicas por tenant não devem permitir acesso a destinos internos. Valide regras globais antes das regras configuráveis.
Teste de loopback
const blocked = [
'http://127.0.0.1',
'http://[::1]',
'http://localhost'
];Todos devem falhar antes da conexão.
Teste de rede privada
Teste endereços de todas as faixas IPv4 e IPv6 proibidas.
Teste de redirect
Crie um servidor público de teste que redireciona para localhost e confirme o bloqueio.
Teste de DNS rebinding
Use um resolvedor controlado que devolve IP público e depois privado. A conexão não pode usar o segundo endereço sem nova validação.
Teste de resposta grande
Envie conteúdo além do limite e confirme cancelamento do stream.
Teste de timeout
Use um servidor que aceita a conexão e não responde. O worker deve liberar os recursos.
Erros comuns
- Bloquear apenas localhost: IPs privados continuam acessíveis.
- Validar antes do redirect: o destino final muda.
- Validar DNS e resolver novamente: rebinding contorna a regra.
- Permitir qualquer porta: serviços internos são alcançados.
- Encaminhar Authorization: segredos vazam.
- Sem limite de corpo: resposta causa DoS.
- Confiar apenas no código: falta isolamento de rede.
Boas práticas
- Prefira allowlist.
- Permita apenas HTTPS.
- Valide porta e credenciais.
- Resolva e valide todos os IPs.
- Controle redirects.
- Bloqueie metadata e redes privadas.
- Use egress firewall.
- Limite tempo e tamanho.
- Não encaminhe segredos.
- Teste bypasses de DNS e IPv6.
Conclusão
Prevenir SSRF no Node.js exige validar o destino real, não apenas a string da URL. Protocolo, porta, DNS, IP e cada redirect precisam permanecer dentro das regras permitidas.
Allowlists, isolamento de rede, limites e headers mínimos formam uma defesa em profundidade. Quando downloads e previews usam um egress controlado, a aplicação pode acessar recursos externos sem transformar o servidor em uma ponte para sua infraestrutura interna.



