OWASP A10 SSRF para Desarrolladores
SSRF permite a atacantes hacer que tu servidor consulte recursos internos — incluyendo credenciales de AWS. Esta guía explica cómo funciona y cómo prevenirlo.
Rod
Founder & Developer
SSRF (Server-Side Request Forgery, o falsificación de solicitudes del servidor) es la entrada más nueva del OWASP Top 10 — se agregó en 2021 como A10. En 2017 no era lo suficientemente prominente para entrar a la lista. Para 2021, OWASP la incluyó basándose en datos de la industria y una realidad muy concreta: la brecha de Capital One en 2019, que expuso más de 100 millones de registros de clientes y comenzó con una sola petición SSRF a un endpoint de metadatos de AWS.
El concepto es directo. Tu servidor hace un fetch a una URL. Un atacante controla cuál es esa URL. Tu servidor termina consultando algo que no debería — servicios internos, credenciales cloud, paneles de administración privados. El atacante está fuera de tu red, pero tu servidor está adentro, y hace las peticiones en su nombre.
Esta guía te muestra exactamente cómo funciona SSRF, cómo se ve el código vulnerable, cómo arreglarlo, y por qué esta clase de vulnerabilidad es cada vez más peligrosa en arquitecturas cloud y de microservicios.
Cómo Funciona SSRF — El Flujo del Ataque
El patrón, en términos simples.
Tu aplicación tiene una funcionalidad que recibe una URL del usuario y la consulta desde el servidor. Quizá es un generador de previsualizaciones de links. Quizás es un validador de webhooks. Tal vez es un proxy de imágenes que redimensiona URLs antes de servirlas. O una función de importación que lee un link de CSV provisto por el usuario.
Todas son funcionalidades legítimas. La vulnerabilidad no está en la funcionalidad en sí — está en qué pasa cuando la URL apunta a algo inesperado.
El flujo del ataque:
- El usuario envía una URL a tu API
- Tu servidor llama a
fetch(url)— oaxios.get(url), o cualquier cliente HTTP - La URL del atacante apunta a
http://169.254.169.254/latest/meta-data/iam/security-credentials/en vez de un sitio real - Tu servidor, corriendo dentro de AWS, hace el fetch — y AWS devuelve credenciales IAM en JSON puro
- El atacante lee la respuesta que tu servidor devuelve, o usa callbacks out-of-band si no devuelves el cuerpo
El endpoint de metadatos de AWS en 169.254.169.254 es el objetivo más peligroso porque está disponible en toda instancia EC2 y devuelve credenciales sin autenticación. Pero el mismo patrón aplica a otros destinos internos:
http://localhost:6379/— Redis, frecuentemente sin autenticación en configs de desarrollo que llegaron a producciónhttp://internal-admin:8080/— un panel de administración interno que asumía que nadie lo podía alcanzar desde internethttp://10.0.0.1/admin— un router o servicio interno en la red privadahttp://kubernetes.default.svc/api/v1/secrets— la API de Kubernetes, si tu app corre en un cluster
Ninguno de esos es accesible desde internet público. Pero sí son accesibles desde adentro de tu servidor. Eso es lo que hace peligroso a SSRF en entornos cloud y microservicios — el perímetro de red detiene a atacantes externos, no a tu propia aplicación haciendo peticiones en su nombre.
SSRF Basado en Redirects
Una variante más sutil: el atacante envía una URL de aspecto legítimo que hace redirect a una dirección interna.
Tu servidor hace fetch de https://dominio-del-atacante.com/redirect. Esa URL devuelve un 301 a http://169.254.169.254/latest/meta-data/. Si tu cliente HTTP sigue redirects automáticamente (la mayoría lo hace por defecto), tu servidor termina en el endpoint interno aunque la URL original parecía inofensiva.
Agregar el dominio inicial a una allowlist no ayuda si sigues el redirect sin revalidar.
Código Vulnerable — Cómo Se Ve SSRF en la Práctica
La versión más directa de la vulnerabilidad. Un API route de Next.js que hace fetch de una URL provista por el usuario:
// MAL: Hace fetch de cualquier URL que el usuario envíe — incluyendo internas
export async function POST(req: Request) {
const { url } = await req.json();
const response = await fetch(url); // sin ningún tipo de validación
const data = await response.text();
return Response.json({ content: data });
}Una sola petición a tu API con url: "http://169.254.169.254/latest/meta-data/iam/security-credentials/mi-rol" y ya entregaste credenciales de AWS.
Una versión menos obvia — un proxy de imágenes que redimensiona imágenes de URLs externas:
// MAL: Proxy de imágenes sin validación de URL
export async function GET(req: Request) {
const { searchParams } = new URL(req.url);
const imageUrl = searchParams.get("url") ?? "";
// Hace fetch de la URL y devuelve el resultado como imagen
// Parece seguro porque "solo son imágenes" — pero no lo es
const imageResponse = await fetch(imageUrl);
const imageBuffer = await imageResponse.arrayBuffer();
return new Response(imageBuffer, {
headers: { "Content-Type": imageResponse.headers.get("Content-Type") ?? "image/jpeg" },
});
}Este patrón es común en codebases reales. El dev pensó "solo estoy descargando imágenes" — pero el servidor no verifica si la URL es realmente de una imagen. Envía http://169.254.169.254/latest/meta-data/ y el cuerpo de la "imagen" son tus credenciales IAM.
Código Seguro — Cómo Bloquear SSRF
El fix tiene tres capas: validar la URL antes del fetch, bloquear rangos de IP privados, y desactivar el seguimiento automático de redirects.
Aquí hay un validador de URLs reutilizable que puedes copiar en cualquier proyecto:
import { isIP } from "net";
// Rangos de IP privados que nunca deben consultarse
const RANGOS_IP_PRIVADOS = [
/^127\./, // localhost
/^10\./, // rango privado RFC 1918
/^172\.(1[6-9]|2\d|3[01])\./, // rango privado RFC 1918
/^192\.168\./, // rango privado RFC 1918
/^169\.254\./, // link-local (aquí viven los metadatos de AWS)
/^::1$/, // localhost IPv6
/^fc00:/, // unique local IPv6
/^fe80:/, // link-local IPv6
];
export function esUrlSegura(rawUrl: string): boolean {
let parsed: URL;
try {
parsed = new URL(rawUrl);
} catch {
return false; // no es una URL válida
}
// Solo permitir http y https — nada de file://, ftp://, gopher://, etc.
if (!["http:", "https:"].includes(parsed.protocol)) return false;
const hostname = parsed.hostname.toLowerCase();
// Bloquear variantes obvias de localhost
if (hostname === "localhost" || hostname === "0.0.0.0") return false;
// Si el hostname es una IP, verificar contra rangos privados
if (isIP(hostname)) {
return !RANGOS_IP_PRIVADOS.some((pattern) => pattern.test(hostname));
}
return true; // hostnames basados en dominio pasan — usa allowlist para control más estricto
}Ahora úsalo antes de cualquier fetch del lado del servidor:
// BIEN: Valida la URL antes del fetch — bloquea acceso a red interna
export async function POST(req: Request) {
const { url } = await req.json();
if (!esUrlSegura(url)) {
return Response.json({ error: "URL no válida" }, { status: 400 });
}
// Desactivar seguimiento de redirects — revalidar cada hop si necesitas redirects
const response = await fetch(url, { redirect: "error" });
const data = await response.text();
return Response.json({ content: data });
}Para el proxy de imágenes, usa una allowlist de dominios permitidos en vez de una blocklist:
// BIEN: Allowlist de dominios + bloqueo de rangos IP para proxy de imágenes
const DOMINIOS_PERMITIDOS = [
"images.unsplash.com",
"cdn.tudominio.com",
"storage.googleapis.com",
];
export async function GET(req: Request) {
const { searchParams } = new URL(req.url);
const imageUrl = searchParams.get("url") ?? "";
let parsed: URL;
try {
parsed = new URL(imageUrl);
} catch {
return new Response("URL no válida", { status: 400 });
}
// Solo permitir dominios explícitamente aprobados
if (!DOMINIOS_PERMITIDOS.includes(parsed.hostname)) {
return new Response("Dominio no permitido", { status: 400 });
}
// Desactivar redirects — un dominio permitido podría redirigir a uno interno
const imageResponse = await fetch(imageUrl, { redirect: "error" });
const imageBuffer = await imageResponse.arrayBuffer();
return new Response(imageBuffer, {
headers: { "Content-Type": "image/webp" },
});
}El problema de los redirects. Usar
redirect: "error"evita que tu cliente HTTP siga cualquier redirect automáticamente. Si tu funcionalidad genuinamente necesita seguir redirects, hazlo manualmente — parsea el headerLocation, llama aesUrlSegura()de nuevo, y luego haz fetch de la siguiente URL. Una sola validación al inicio no alcanza si la primera URL redirige a una dirección interna.
SSRF en el Mundo Real: La Brecha de Capital One
En julio de 2019, un Web Application Firewall (WAF) mal configurado en AWS le permitió a un atacante ejecutar peticiones del lado del servidor hacia el Instance Metadata Service de EC2.
El atacante envió una petición manipulada que el WAF interpretó como legítima y reenvió a la instancia EC2. El proceso del WAF corría con un rol IAM adjunto. La petición SSRF llegó a http://169.254.169.254/latest/meta-data/iam/security-credentials/ y devolvió las credenciales temporales del rol — access key ID, secret access key y session token — en JSON puro.
Con esas credenciales, el atacante accedió a buckets de S3 con los datos de los clientes de Capital One. El total: más de 106 millones de registros de clientes de Estados Unidos y Canadá. Capital One pagó una multa de $190 millones a la FTC. La brecha está documentada públicamente en los hallazgos de la investigación de la FTC.
El exploit inicial — la petición SSRF al endpoint de metadatos — fue una sola petición HTTP con una URL manipulada. El daño fue proporcional al nivel de acceso que tenía el rol IAM, no a qué tan sofisticado fue el ataque inicial.
Dos lecciones. Primera: SSRF no es una vulnerabilidad teórica. Segunda: los endpoints de metadatos cloud son el objetivo de mayor valor en cualquier escenario SSRF porque entregan credenciales, no solo datos.
El CVE-2021-22214 de GitLab es un segundo ejemplo útil. La vulnerabilidad SSRF le permitía a atacantes no autenticados hacer peticiones HTTP desde el servidor de GitLab a servicios internos. Fue calificada como Crítica (CVSS 8.6) y no requería autenticación para explotarla — solo una URL de webhook manipulada enviada a través de la API pública.
Prevención: Cinco Cosas Que Hacer Ahora
Si estás escaneando tu codebase o revisando un PR, estos son los controles específicos que bloquean SSRF:
1. Valida las URLs Antes del Fetch
Nunca pases strings controlados por el usuario directamente a fetch(), axios.get(), got(), o cualquier cliente HTTP. Siempre parsea y valida primero. La función esUrlSegura() de arriba es un punto de partida — adáptala a tu stack.
2. Usa Allowlists de Dominios, No Blocklists
Las blocklists siempre tienen huecos. Se agregan nuevos servicios internos. Aparecen nuevos vectores de ataque. Una allowlist que solo permite images.tudominio.com y cdn.cloudfront.net no se puede evadir encontrando un dominio que olvidaste bloquear.
3. Desactiva el Seguimiento Automático de Redirects
// Node.js fetch — desactivar redirects
const response = await fetch(url, { redirect: "error" });
// axios — desactivar redirects
const response = await axios.get(url, { maxRedirects: 0 });Si tu funcionalidad genuinamente necesita seguir redirects, construye un loop que llame a esUrlSegura() en cada header Location antes de seguirlo.
4. Habilita IMDSv2 en AWS
IMDSv2 (Instance Metadata Service v2) requiere un session token que se obtiene haciendo primero una petición PUT. Una petición SSRF GET simple a 169.254.169.254 no puede obtener el token en un solo paso. Esta es la mitigación a nivel de AWS — no arregla tu código, pero limita el impacto potencial.
Habilitarlo en tu configuración de EC2:
# Requerir IMDSv2 — petición PUT necesaria antes de que funcionen los GET
aws ec2 modify-instance-metadata-options \
--instance-id i-1234567890abcdef0 \
--http-tokens required \
--http-endpoint enabledEn Terraform, configura http_tokens = "required" en el bloque metadata_options de tu recurso aws_instance.
5. Agrega Controles a Nivel de Red
La validación a nivel de aplicación es tu primera línea de defensa. Los controles de red son el respaldo. Configura tus reglas de VPC o firewall para bloquear peticiones salientes desde tus servidores hacia el rango IP de metadatos (169.254.169.254/32) y hacia tus subredes privadas internas.
Seguridad en capas: si la validación en código falla, la regla de red lo detiene.
Dónde Se Esconde SSRF en Tu Codebase
Cuando escaneamos repos en Data Hogo, el riesgo de SSRF aparece más frecuentemente en estos patrones:
Endpoints de registro de webhooks. Permites que los usuarios registren una URL de webhook que tu servidor llama en ciertos eventos. Cualquier URL controlada por el usuario en una petición HTTP del lado del servidor es un vector SSRF potencial si no se valida.
Generadores de previsualizaciones de links. La app hace fetch de la URL para extraer metadatos Open Graph. Rápido de construir, fácil de olvidar validar.
Servicios de PDF y screenshots. Los headless browsers (Puppeteer, Playwright) que renderizan URLs del usuario son SSRF con pasos extra — la página renderizada también puede filtrar contenido interno a través de recursos embebidos.
Funciones de importación y sincronización de datos. "Pega una URL de un archivo CSV y lo importamos." El servidor hace fetch del CSV. La URL del CSV podría ser http://servicio-interno/datos-sensibles.csv.
Integraciones con terceros. Hacer fetch de un logo desde un dominio provisto por el usuario. Validar una URL de un formulario. Cualquier lugar donde el usuario provea un string que se convierte en un destino de red.
La entrada enciclopédica sobre SSRF cubre la taxonomía completa incluyendo técnicas de SSRF out-of-band. Si estás construyendo funcionalidades que aceptan URLs, lee también SSRF en APIs para patrones específicos de APIs REST y GraphQL, y SSRF con metadatos cloud para los endpoints específicos de AWS, GCP y Azure que necesitas bloquear.
Revisar Tu Código para Detectar SSRF
El scanner de Data Hogo detecta patrones propensos a SSRF: llamadas a fetch(), axios.get(), got(), request(), y clientes HTTP similares donde el argumento URL tiene su origen en input del usuario — cuerpo de la petición, query parameters, path parameters, o fuentes de datos externas.
Marcamos estos como findings de riesgo SSRF con el archivo y línea específicos, el cliente HTTP usado, y si la URL parece validarse antes de usarse. Corregir un finding marcado generalmente es agregar un paso de validación — lo puedes hacer en minutos una vez que sabes dónde buscar.
¿Tu codebase tiene funcionalidades que hacen fetch de URLs del usuario? Escanea tu repo gratis para encontrar patrones SSRF.
Preguntas Frecuentes
¿Qué es la falsificación de solicitudes del servidor (SSRF)?
SSRF es una vulnerabilidad donde un atacante engaña a tu servidor para que haga peticiones HTTP a una dirección que controla. Tu servidor hace un fetch a una URL que parece legítima pero en realidad apunta a un servicio interno — como el endpoint de metadatos de AWS en 169.254.169.254, una instancia de Redis, o un panel de administración interno. El servidor hace la petición, así que las reglas de firewall que bloquean usuarios externos no sirven. Es el OWASP A10:2021.
¿Por qué es peligroso el endpoint de metadatos de AWS en 169.254.169.254?
El Instance Metadata Service (IMDS) de AWS es un endpoint link-local disponible para cualquier proceso que corra en una instancia EC2. Devuelve las credenciales del rol IAM — access key, secret key y session token — en JSON puro, sin autenticación. Si tu servidor hace fetch de URLs controladas por el usuario y no bloquea 169.254.169.254, una sola petición entrega credenciales que pueden acceder a toda tu cuenta de AWS. Habilitar IMDSv2 mitiga esto al requerir un session token que un GET simple no puede obtener.
¿Cuál es la diferencia entre SSRF básico y SSRF ciego?
En el SSRF básico, el servidor devuelve el contenido que consultó al atacante — puede leer la respuesta del recurso interno directamente. En el SSRF ciego, el servidor hace la petición pero no devuelve el cuerpo de la respuesta. El atacante igual puede confirmar que hosts internos existen (midiendo tiempos de respuesta o usando callbacks out-of-band), pero no puede leer los datos directamente. El SSRF ciego es más difícil de explotar, pero sirve para mapear la infraestructura interna.
¿Cómo bloqueo SSRF en una aplicación Node.js o Next.js?
Parsea la URL antes de hacer el fetch. Verifica que el hostname no sea un rango IP privado (10.x.x.x, 172.16-31.x.x, 192.168.x.x, 127.x.x.x) ni una dirección link-local (169.254.x.x). Usa una allowlist de dominios permitidos en vez de una blocklist. Desactiva el seguimiento automático de redirects HTTP, o re-valida el hostname después de cada redirect. Nunca pases strings controlados por el usuario directamente a fetch(), axios, u otro cliente HTTP.
¿El SSRF causó la brecha de Capital One?
Sí. La brecha de Capital One en 2019 empezó con una vulnerabilidad SSRF en un Web Application Firewall mal configurado. El atacante lo usó para alcanzar el endpoint de metadatos de AWS en 169.254.169.254, obtuvo credenciales IAM, y las usó para acceder a más de 100 millones de registros de clientes en S3. Es el ejemplo más citado de SSRF en el mundo real porque el impacto — 106 millones de registros, una multa de $190 millones — es proporcional a lo simple que fue el exploit inicial.
Posts Relacionados
OWASP A03 Ataques de Inyección
OWASP A03:2021 Inyección cubre SQL, NoSQL, XSS, inyección de comandos y más. Código vulnerable vs. seguro en Node.js, un breach real, y cinco estrategias de prevención.
OWASP A05 Configuración Insegura
El 90% de las aplicaciones tiene alguna configuración insegura. Aprende qué cubre OWASP A05:2021, ve el código vulnerable vs. seguro en Next.js, y corrígelo ya.
OWASP A01 Control de Acceso Roto
El control de acceso roto es el riesgo #1 del OWASP Top 10. Esta guía explica IDOR, JWT tampering y rutas sin auth — con ejemplos reales en Next.js y cómo corregirlos.