SSRF Guards
Previna Server-Side Request Forgery - bloqueie buscas para IPs internos, endpoints de metadados da nuvem e redes privadas quando os usuários fornecem URLs.
Busque em todas as páginas da documentação
Previna Server-Side Request Forgery - bloqueie buscas para IPs internos, endpoints de metadados da nuvem e redes privadas quando os usuários fornecem URLs.
Cartão de receita de referência rápida - pronto para copiar e colar.
import { lookup } from "node:dns/promises";
import ipaddr from "ipaddr.js";
const BLOCKED = new Set(["private", "loopback", "linkLocal", "uniqueLocal", "carrierGradeNat"]);
async function assertSafeUrl(raw: string): Promise<URL> {
const url = new URL(raw);
if (!["http:", "https:"].includes(url.protocol)) throw new Error("Invalid protocol");
const { address } = await lookup(url.hostname);
const range = ipaddr.parse(address).range();
if (BLOCKED.has(range)) throw new Error("Blocked destination");
return url;
}Quando usar isso:
fetch(userSuppliedUrl) na base de código.import { lookup } from "node:dns/promises";
import ipaddr from "ipaddr.js";
const METADATA_HOSTS = new Set(["169.254.169.254", "metadata.google.internal"]);
const ALLOWED_HOSTS = new Set(["api.stripe.com", "hooks.slack.com"]);
const BLOCKED_RANGES = new Set([
"private", "loopback", "linkLocal", "uniqueLocal", "carrierGradeNat", "multicast",
]);
async function resolveAndValidate(hostname: string): Promise<void> {
if (METADATA_HOSTS.has(hostname)) throw new SsrfError("Blocked host");
const records = await lookup(hostname, { all: true });
for (const { address } of records) {
const range = ipaddr.parse(address).range();
if (BLOCKED_RANGES.has(range)) throw new SsrfError(`Blocked IP range: ${range}`);
}
}
class SsrfError extends Error {
constructor(message: string) {
super(message);
this.name = "SsrfError";
}
}
async function safeFetch(raw: string, init?: RequestInit): Promise<Response> {
const url = new URL(raw);
if (!["http:", "https:"].includes(url.protocol)) throw new SsrfError("Invalid protocol");
if (process.env.SSRF_ALLOWLIST === "true" && !ALLOWED_HOSTS.has(url.hostname)) {
throw new SsrfError("Host not allowlisted");
}
await resolveAndValidate(url.hostname);
return fetch(url, {
...init,
redirect: "manual",
signal: AbortSignal.timeout(init?.signal ? undefined : 5_000),
});
}
// Rota Express
import express from "express";
import { z } from "zod";
const app = express();
app.use(express.json());
app.post("/import", async (req, res) => {
const { sourceUrl } = z.object({ sourceUrl: z.string().url() }).parse(req.body);
try {
const response = await safeFetch(sourceUrl);
if (response.status >= 300 && response.status < 400) {
return res.status(400).json({ error: "Redirects not allowed" });
}
res.json({ size: (await response.arrayBuffer()).byteLength });
} catch (err) {
if (err instanceof SsrfError) return res.status(400).json({ error: err.message });
throw err;
}
});O que isso demonstra:
10.x ou 127.0.0.1.redirect: "manual" impede bypass via 302 para IP interno.AbortSignal.timeout limita chamadas de saída pendentes.169.254.169.254 expõem credenciais IAM na AWS.| Camada | Controle |
|---|---|
| Protocolo | Apenas http: e https: - bloqueie file:, gopher: |
| Nome do host | Bloqueie hosts de metadados; permita quando possível |
| DNS | Resolva todos os registros A/AAAA; bloqueie intervalos privados |
| Redirecionamento | manual + rejeite 3xx ou revalide Location |
| Saída | Política de rede / firewall de saída (defesa em profundidade) |
const parsed = ipaddr.parse(address);
if (parsed.kind() === "ipv6") {
const ipv4 = parsed.isIPv4MappedAddress() ? parsed.toIPv4Address() : parsed;
// verifique o intervalo na forma mapeada
}::1, link-local fe80::/10 e endereços privados mapeados para IPv4.const ALLOWED = /\.stripe\.com$/;
if (!ALLOWED.test(url.hostname)) throw new SsrfError("Not allowed");internal.service resolve para 10.x. Correção: sempre resolva o DNS.redirect: "manual".http://127.0.0.1. Correção: bloqueie ou desabilite hosts de IP inteiramente.safeFetch centralizado.fetch( e axios.get em CI.| Alternativa | Use Quando | Não Use Quando |
|---|---|---|
| Resolução de DNS + bloqueio de IP | Recursos gerais de importação de URL | Conjunto fixo de parceiros (use lista de permissão) |
| Lista de permissão de domínio | Provedores de webhook conhecidos | URLs arbitrárias do usuário |
| Proxy de saída | Rede corporativa zero-trust | MVP SaaS simples |
| Sem busca no lado do servidor | O cliente pode baixar diretamente | Necessidade de processamento no servidor |
Não. new URL() aceita http://10.0.0.1 e http://169.254.169.254. Resolva e bloqueie intervalos.
Mesmas regras: httpAgent personalizado, maxRedirects: 0, valide a URL antes da solicitação.
Sim, por padrão. Passe redirect: "manual" quando o usuário fornecer a URL.
Pacotes como ssrf-req-filter ajudam. Ainda adicione lista de permissão e timeouts para defesa em profundidade.
Não é SSRF se a URL for controlada por configuração, não fornecida pelo usuário. Use mTLS e DNS privado.
Entrada: verifique as assinaturas. SSRF de saída é quando VOCÊ busca URLs de usuários - ameaça diferente.
Bloqueie 169.254.169.254 e restrinja a saída de pods com NetworkPolicy.
Testes unitários com 127.0.0.1, 10.0.0.1 e IP de metadados - espere rejeição antes que o mock de busca seja acionado.
Versões da Stack: Esta página foi escrita para Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5 e NestJS 11.
Revisado por Chris St. John·Última atualização: 19 de jul. de 2026