SSRF e IMDSv2
Cómo un SSRF hace que el servidor pida la URL del atacante, por qué en AWS puede robar las credenciales del rol vía 169.254.169.254, y cómo IMDSv2 y el egress lo cortan.
De todas las vulnerabilidades web, el SSRFSSRFFallo en el que un atacante hace que el servidor emita peticiones a una URL que él controla, alcanzando sistemas internos que él no podría tocar. es la que mejor conecta la aplicación con la nube. Un fallo que en un servidor tradicional sería “molesto” —hacer que el servidor pida una URL que tú eliges— en AWS puede convertirse en el robo de las credenciales del rol de la instancia y, a partir de ahí, en el compromiso de la cuenta. Es el caso que hizo famoso a IMDSv2, y la razón por la que esta nota es la pieza clave del tema. Como siempre, todo lo que sigue es para defender: pruébalo solo en tu laboratorio.
Qué es SSRF#
Server-Side Request Forgery (A10 en el OWASP Top 10) ocurre cuando una aplicación toma una URL —o parte de ella— de la entrada del usuario y la solicita desde el servidor. Muchas funciones legítimas lo hacen: previsualizar un enlace, importar una imagen desde una URL, un webhook, un conversor de documentos. Si el servidor pide lo que le digan sin restricción, el atacante lo usa como proxy hacia sitios que él no alcanza directamente:
- Servicios en subredes privadas (bases de datos, paneles internos) que no están expuestos a Internet.
localhosty puertos locales del propio servidor (http://127.0.0.1:8080/admin).- En la nube, el servicio de metadatos de la instancia.
La clave es el cambio de perspectiva: la petición ya no sale del navegador del atacante, sale del servidor, desde dentro de la red y con su identidad.
Paso 1 de 5La petición
La app de Citas tiene una función que descarga una URL que le pasa el usuario (previsualizar un adjunto). El atacante le pasa la IP del servicio de metadatos.
Por qué SSRF y nube van de la mano#
Toda instancia EC2 puede consultar un servicio interno de metadatos (Instance Metadata Service, IMDS) en la dirección link-local 169.254.169.254 (en IPv6, fd00:ec2::254). Ese servicio expone datos de la instancia y, lo más sensible, las credenciales temporales del rol IAM asociado, en la ruta:
http://169.254.169.254/latest/meta-data/iam/security-credentials/<nombre-del-rol>Esa ruta devuelve un AccessKeyId, un SecretAccessKey y un Token de sesión. Quien los lea puede actuar como la instancia frente a la API de AWS, con todos los permisos de su rol. La dirección es link-local: no se enruta, no sale de la máquina, y por eso durante años se consideró “segura” sin autenticación. El SSRF rompe ese supuesto, porque la petición la hace el propio servidor.
Con IMDSv1, cualquier GET bastaba. Un SSRF tan simple como ?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/ leía las credenciales y las devolvía al atacante. Es, en esencia, lo que ocurrió en la brecha de Capital One (2019): un SSRF contra un componente mal configurado en EC2 alcanzó el servicio de metadatos, obtuvo las credenciales del rol y las usó para leer datos en S3. AWS lanzó IMDSv2 poco después, en parte como respuesta.
IMDSv2: por qué existe y cómo funciona#
IMDSv2 convierte el acceso a los metadatos en una sesión que un SSRF simple no puede completar. En vez de un GET directo, exige dos pasos:
-
Obtener un token con una petición
PUTy una cabecera especial. El token dura lo que indiques, hasta un máximo de 6 horas (21 600 s):Terminal TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" \-H "X-aws-ec2-metadata-token-ttl-seconds: 21600") -
Usar el token en cada
GET, en la cabeceraX-aws-ec2-metadata-token:Terminal curl -H "X-aws-ec2-metadata-token: $TOKEN" \http://169.254.169.254/latest/meta-data/iam/security-credentials/rol-de-citas
Esto frena el SSRF por varios motivos a la vez:
- Exige un
PUT. La mayoría de primitivas SSRF solo saben hacerGET(el atacante controla una URL, no el método). Sin elPUTinicial no hay token, y sin token elGETrecibe 401 Unauthorized. - Rechaza cabeceras de proxy. El servicio rechaza los
PUTque llevan una cabeceraX-Forwarded-For, típica cuando la petición pasa por un proxy o se reenvía —justo el patrón de muchos SSRF. - Límite de saltos (hop limit). La respuesta al
PUTtiene por defecto un hop limit de 1 a nivel IP: el token no puede reenviarse más allá de la propia instancia. (Para contenedores que añaden un salto de red, a veces hace falta subirlo a 2.)
Cómo forzar IMDSv2#
Que IMDSv2 exista no basta: hay que exigirlo para que IMDSv1 deje de responder. Se hace con HttpTokens=required en las opciones de metadatos, al lanzar la instancia o modificándola:
resource "aws_instance" "worker" { # ... metadata_options { http_endpoint = "enabled" http_tokens = "required" # exige IMDSv2; IMDSv1 deja de funcionar http_put_response_hop_limit = 1 # 2 si corres contenedores }}# Exigir IMDSv2 en una instancia existenteaws ec2 modify-instance-metadata-options \ --instance-id i-0123456789abcdef0 \ --http-tokens required \ --http-endpoint enabled
# Encontrar instancias que todavía permiten IMDSv1 (optional)aws ec2 describe-instances \ --query 'Reservations[].Instances[?MetadataOptions.HttpTokens==`optional`].InstanceId' \ --output textOtras defensas (no dependas solo de IMDSv2)#
IMDSv2 protege un objetivo concreto —los metadatos—, pero el SSRF puede apuntar a muchos otros. La defensa es en capas, empezando por la aplicación:
No confíes en la entrada para construir URLs#
El origen del problema es pedir lo que el usuario diga. En el lado de la app:
Vulnerable
Seguro
// Pide cualquier URL que llegue en el parámetro: SSRF directoconst res = await fetch(req.query.url);return res.body;// Allow-list de destinos + bloqueo de direcciones internas/link-localconst url = new URL(req.query.url);if (url.protocol !== 'https:') throw new Forbidden();if (!ALLOWED_HOSTS.has(url.hostname)) throw new Forbidden();// Resuelve el DNS y rechaza rangos privados y 169.254.0.0/16 (anti-rebind)await assertPublicAddress(url.hostname);const res = await fetch(url, { redirect: 'manual' }); // no sigas redirecciones a ciegasPuntos finos que marcan la diferencia: usa allow-list, no deny-list (es fácil olvidar un rango); resuelve el DNS y valida la IP resuelta, no solo el nombre (contra DNS rebinding); y no sigas redirecciones automáticamente, porque un 302 a 169.254.169.254 evade una validación que solo mire la URL original.
Egress allow-list y VPC endpoints#
Como segunda capa, controla la salida: si la tarea de Citas solo puede conectarse a los destinos que necesita (vía security groups de salida restringidos, un proxy de egress o VPC endpoints para hablar con AWS sin salir a Internet), una petición al rango de metadatos o a un host arbitrario no llega. Es reducir la superficie por el lado de la salida, y no depende de que la app esté bien escrita.
WAF y detección#
AWS WAF tiene reglas que bloquean patrones de SSRF conocidos (URLs con IPs internas), útil como filtro externo pero evadible: trátalo como detección. Y con A09 cubierto, vigila en los logs las peticiones salientes hacia 169.254.169.254 o rangos privados: en una app sana no deberían existir.
Tu turno
Audita el acceso a metadatos en tu entorno
Sobre una cuenta de AWS tuya o de tu laboratorio:
- Lista tus instancias EC2 y comprueba cuáles tienen
HttpTokens=optional(el comando de la pestaña AWS CLI). ¿Cuántas siguen permitiendo IMDSv1? - Desde una instancia tuya, reproduce el flujo IMDSv2 con los dos
curly observa la respuesta de credenciales. Luego ponhttp_tokens = "required"y comprueba que unGETsin token devuelve 401. - Revisa la salida a Internet de una de tus cargas: ¿podría alcanzar
169.254.169.254o una subred interna? ¿Qué allow-list de egress lo impediría? - Busca en tu código funciones que hagan
fetch/requestcon una URL que venga del usuario. ¿Validan el destino?
Reto
Tienes IMDSv2 obligatorio. ¿Puedes olvidarte del SSRF?
▸Mostrar solución
No. IMDSv2 protege los metadatos, pero el SSRF puede seguir alcanzando servicios internos,
localhost, paneles de administración en subredes privadas o bases de datos. Y si subes el hop
limit a 2 para contenedores sin más controles, reabres parte del riesgo. IMDSv2 es una capa
crucial, no la única: hace falta validar la entrada en la app y controlar el egress.
Reto
¿Por qué exigir un PUT con una cabecera frena a casi todos los SSRF, si técnicamente un atacante podría hacer un PUT?
▸Mostrar solución
Porque la mayoría de primitivas SSRF dan control sobre una URL, no sobre el método HTTP ni las
cabeceras. El atacante consigue que el servidor haga un GET url, pero no que haga un PUT con
X-aws-ec2-metadata-token-ttl-seconds. Los pocos casos donde sí controla método y cabeceras
(algunas inyecciones de petición completas) son mucho más raros, y ahí entran el hop limit y el
rechazo de X-Forwarded-For. IMDSv2 no es infalible; sube el listón lo bastante para neutralizar
el SSRF típico.
En resumen#
- SSRF: la app pide una URL que controla el atacante, usando al servidor como proxy hacia lo que él no alcanza.
- En la nube es crítico porque puede leer
169.254.169.254y obtener las credenciales del rol de la instancia (así fue Capital One). - IMDSv2 convierte el acceso en una sesión:
PUTpara un token (máx. 6 h),GETcon la cabecera del token, hop limit 1 y rechazo deX-Forwarded-For. Un SSRF simple no lo completa. - Exígelo con
HttpTokens=required(Terraform, CLI o por defecto en la cuenta) y marca lo no conforme con Config o SCPs. - No dependas solo de IMDSv2: valida la entrada de URLs (allow-list, IP resuelta, sin redirecciones) y controla el egress.