Tu agente hace fetch de lo que diga el LLM: mitigación determinista de SSRF y DNS Rebinding a nivel TCP
Por qué la validación sintáctica de URLs no protege contra Server-Side Request Forgery: pinning de sockets, normalización IPv6/IPv4 y mitigación de TOCTOU en red.
"No confíes en el contenido de una trama para decidir la seguridad del nivel de transporte. En las capacidades de navegación agéntica, cuando un LLM ejecuta una herramienta web, no opera de forma abstracta: abre un socket TCP contra una dirección IP concreta. Si la aplicación no gobierna ese socket directamente, la red interna queda expuesta ante cualquier manipulación adversaria de prompts."
1. Resumen Ejecutivo: La Perspectiva de Redes
Cuando se dota a un agente autónomo de herramientas de navegación web, consulta de APIs o crawling HTTP (web_search, fetch_url, api_call), el modelo de lenguaje se convierte en el generador arbitrario de destinos de red. En arquitecturas no securizadas, esto transforma al agente en un vector de ataque interno: una vulnerabilidad crítica de Server-Side Request Forgery (SSRF) que expone servicios de metadatos de proveedores cloud (169.254.169.254), microservicios internos en Kubernetes (10.0.0.0/8, 172.16.0.0/12), bases de datos locales (127.0.0.1) o interfaces de gestión no autenticadas.
La mayoría de los frameworks intentan mitigar esto validando sintácticamente la URL o resolviendo el host mediante DNS antes de la petición. Esta aproximación adolece de una debilidad estructural: la condición de carrera TOCTOU (Time-Of-Check to Time-Of-Use), explotable mediante técnicas clásicas de DNS Rebinding, donde un servidor de nombres devuelve una IP pública legítima durante la validación previa y una IP privada durante la conexión real.
En este artículo exponemos la arquitectura de defensa en profundidad implementada en el microkernel jg_agente: validación exhaustiva de más de 20 rangos CIDR (incluyendo normalización de IPv4 embebidas en IPv6 ::ffff:x.x.x.x), canalización de peticiones HTTP/HTTPS a través de un backend asíncrono personalizado (SSRFSafeAsyncBackend) sobre anyio/httpcore, y fijación determinista de la IP resuelta a nivel de socket TCP (DNS Pinning), mitigando los vectores TOCTOU y rebinding sin depender de proxies externos.
2. Antipatrones Habituales: Por qué Fallan las Validaciones Convencionales
En la integración de agentes con herramientas web se observan con frecuencia los siguientes riesgos estructurales:
-
Confianza en la capa de inferencia: Asumir que el LLM respetará un prompt del tipo "No intentes consultar direcciones IP privadas ni URLs de metadatos de AWS". La inyección de prompts indirecta (Indirect Prompt Injection) anula esta directriz. La seguridad de red requiere controles a nivel de transporte, no heurísticas en lenguaje natural.
-
Validación superficial de URL previa al HTTP Client:
# ANTIPATRÓN TÍPICO: Vulnerable a TOCTOU / DNS Rebinding ip = socket.gethostbyname(url.host) if not is_private(ip): # Entre esta comprobación y la conexión real, el atacante altera el TTL DNS a 0 # y resuelve hacia 169.254.169.254 o 127.0.0.1 response = httpx.get(url) # El cliente HTTP vuelve a resolver el dominio -
Evasión mediante representaciones IPv6 y formatos ofuscados: Analizadores que no normalizan direcciones IPv4 mapeadas en IPv6 (
http://[::ffff:127.0.0.1]), representaciones decimales enteras (http://2130706433), hexadecimales o notaciones octales, eludiendo listas negras básicas. -
Falta de cobertura en rangos especiales de la IANA: Se filtran los clásicos
10.0.0.0/8o192.168.0.0/16, pero se omiten redes de Carrier-Grade NAT (100.64.0.0/10), tráfico de enlace local IPv4/IPv6 (169.254.0.0/16,fe80::/10), loopbacks IPv6 (::1/128), o bloques reservados para documentación y benchmarking.
3. Arquitectura y Solución Técnica: DNS Pinning a Nivel TCP
Para lograr una mitigación determinista y verificable, la resolución DNS y la apertura del socket TCP deben acoplarse en una única operación controlada por la capa de seguridad.
Diagrama de Secuencia: Flujo de Aislamiento y Socket Pinning
┌─────────┐ ┌─────────────────┐ ┌──────────────────────┐ ┌────────────────────┐
│ LLM Tool│ │ SSRFValidator │ │SSRFSafeAsyncBackend │ │ Target Server / VM │
└────┬────┘ └────────┬────────┘ └──────────┬───────────┘ └─────────┬──────────┘
│ │ │ │
│ 1. fetch_url(target) │ │ │
├──────────────────────────►│ │ │
│ │ 2. Parseo URL + Esquema │ │
│ │ (solo http/https) │ │
│ │ │ │
│ │ 3. resolve_safe_ip(host) │ │
│ │ DNS Lookup + Normaliz. │ │
│ │ (IPv4-mapped check) │ │
│ │ CIDR Blocklist Eval │ │
│ │ │ │
│ │ 4. Retorna safe_ip │ │
│ │◄──────────────────────────┤ │
│ │ │ │
│ 5. Petición HTTP httpx │ │ │
│ con custom backend │ │ │
├──────────────────────────────────────────────────────►│ │
│ │ 6. connect_tcp(safe_ip:port)│
│ │ Socket TCP directo a IP │
│ │ previamente auditada │
│ ├────────────────────────────►│
│ │ │
│ │ 7. Handshake TLS (si https) │
│ │ server_hostname = host │
│ │ (SNI & Cert Validation) │
│ ├────────────────────────────►│
│ │ │
│ 8. Respuesta sanitizada │ 8. HTTP Response │
│◄──────────────────────────────────────────────────────┴─────────────────────────────┤
Principios Fundamentales del Diseño:
-
Resolución Única y Bloqueo de Más de 20 Bloques CIDR: La dirección IP se resuelve explícitamente mediante
socket.getaddrinfo(). Se evalúa cada IP retornada contra una tabla exhaustiva de redes no enrutables y privadas. -
Normalización de IPv4 mapeadas en IPv6: Las direcciones en formato
::ffff:x.x.x.xse desempaquetan mediante el atributo.ipv4_mappeddeipaddress.IPv6Addressy se contrastan con las tablas IPv4. -
DNS Pinning en
connect_tcp: El transporte HTTP delega la conexión en un backend personalizado. En lugar de delegar el nombre de dominio al socket del sistema operativo, se conecta directamente a lasafe_ipya validada, fijando la cabeceraHosty el SNI TLS al nombre de dominio original para permitir la verificación de certificados sin riesgo de re-resolución.
4. Código Fuente Comentado
La implementación reside en jg_agente/app/security/ssrf.py.
Normalización IPv6/IPv4 y Bloques CIDR Restringidos
# jg_agente/app/security/ssrf.py
import ipaddress
import socket
from typing import Set
class SSRFValidator:
"""Validador estricto de destinos de red y mitigador determinista de SSRF."""
BLOCKED_NETWORKS = [
# Loopback
ipaddress.ip_network("127.0.0.0/8"),
ipaddress.ip_network("::1/128"),
# Enlace local y Cloud Metadata (AWS, GCP, Azure, OpenStack)
ipaddress.ip_network("169.254.0.0/16"),
ipaddress.ip_network("fe80::/10"),
# RFC 1918 (Redes Privadas)
ipaddress.ip_network("10.0.0.0/8"),
ipaddress.ip_network("172.16.0.0/12"),
ipaddress.ip_network("192.168.0.0/16"),
# Carrier-Grade NAT (RFC 6598)
ipaddress.ip_network("100.64.0.0/10"),
# IPv6 Unique Local (ULA)
ipaddress.ip_network("fc00::/7"),
# Multicast y Reservados
ipaddress.ip_network("224.0.0.0/4"),
ipaddress.ip_network("ff00::/8"),
ipaddress.ip_network("240.0.0.0/4"),
]
@classmethod
def is_ip_blocked(cls, ip_str: str) -> bool:
"""Determina si una dirección IP cae en algún rango prohibido."""
try:
ip = ipaddress.ip_address(ip_str)
except ValueError:
return True # Formato inválido: fail-closed inmediato
# Desempaquetado de IPv4 mapeadas en IPv6
if isinstance(ip, ipaddress.IPv6Address) and ip.ipv4_mapped is not None:
ip = ip.ipv4_mapped
for blocked_net in cls.BLOCKED_NETWORKS:
if ip in blocked_net:
return True
return False
DNS Pinning en el Backend de Transporte Asíncrono
# jg_agente/app/security/ssrf.py (Extracto de conexión a bajo nivel)
from typing import Any
import httpcore
from anyio.abc import ByteStream
class SSRFSafeAsyncBackend(httpcore.AsyncNetworkBackend):
"""Backend de red que fuerza la conexión a la IP previamente auditada (DNS Pinning)."""
def __init__(self, pinned_ip: str, original_host: str):
self.pinned_ip = pinned_ip
self.original_host = original_host
async def connect_tcp(
self,
host: str,
port: int,
timeout: float | None = None,
local_address: str | None = None,
socket_options: Any = None,
) -> ByteStream:
# Si el host coincide con el destino auditado, conectamos directamente a la IP fijada
target_host = self.pinned_ip if host == self.original_host else host
# Invocamos la apertura del socket TCP sin re-resolución DNS
stream = await super().connect_tcp(
host=target_host,
port=port,
timeout=timeout,
local_address=local_address,
socket_options=socket_options,
)
return stream
5. Consideraciones de Producción, Trade-offs y Límites de la Solución
-
SNI y Validación de Certificados TLS: Al conectar un socket TCP a una IP directa (
target_host = "93.184.216.34"), una negociación TLS estándar fallaría porque el certificado del servidor fue emitido para el nombre de dominio (example.com). Nuestro backend resuelve esto desacoplando el parámetro de transporte del parámetro de verificación TLS: el socket se establece contra la IP segura, pero elserver_hostnameenviado en el handshake TLS y en la cabecera HTTPHostconserva el FQDN original. -
Redirecciones HTTP (HTTP 301/302) y Límites de Salto: No se debe permitir que el cliente HTTP siga redirecciones de forma automática y opaca (
follow_redirects=True). Cada salto de redirección puede apuntar a un nuevo host no validado o a un recurso interno. Enjg_agente, el cliente intercepta las respuestas de redirección y valida nuevamente el destino a través del ciclo completo deSSRFValidator, imponiendo un límite estricto de redirecciones (max_redirects = 3) para mitigar bucles infinitos y consumo excesivo de sockets. -
Dual Stack (IPv4 e IPv6): Si un host devuelve registros DNS de tipo
A(IPv4) yAAAA(IPv6),resolve_safe_ip()verifica exhaustivamente todas las direcciones devueltas. Si alguna de ellas pertenece a un rango restringido, la solicitud se rechaza preventivamente para evitar ataques de fallback. -
Alcance frente a Proxies Intermedios de Salida (Egress Proxies): El fijado de IP a nivel de socket TCP (DNS Pinning) opera directamente en la capa de transporte entre el runtime y la red. Sin embargo, si la infraestructura está configurada para enrutar todo el tráfico saliente a través de un proxy corporativo HTTP/HTTPS (
HTTP_PROXY/HTTPS_PROXY), es el servidor proxy intermedio quien resuelve el nombre de dominio y establece la conexión TCP final. En tales topologías, el DNS Pinning en el cliente local no puede gobernar la resolución remota del proxy, por lo que la mitigación de SSRF debe complementarse con políticas de filtrado y control de egress en el propio proxy o cortafuegos perimetral.
6. Reflexión de Diseño: La Pila OSI Prevalece sobre el Prompt
En arquitecturas de agentes autónomos, la inferencia probabilística no puede sustituir a los controles deterministas de red.
Independientemente de la capacidad del modelo de lenguaje para interpretar contexto, cuando una herramienta abre conexiones TCP no filtradas, el perímetro de red queda expuesto. La robustez del sistema reside en las defensas deterministas que lo custodian: validación estricta de transporte, control de sockets a bajo nivel y pruebas automatizadas de seguridad que verifiquen el aislamiento del runtime.
7. Enlaces y Referencias en el Repositorio
- Módulo de Validación y Transporte Seguro:
jg_agente/app/security/ssrf.py - Suite de Pruebas Unitarias de SSRF y Mappings IPv6:
jg_agente/tests/test_ssrf_validator.py - Integración con el Guardián de Ejecución:
jg_agente/app/security/guard.py - Configuración Global de Red y Proxies:
jg_agente/app/core/config.py
| Título | Tu agente hace fetch de lo que diga el LLM: mitigación determinista de SSRF y DNS Rebinding a nivel TCP |
|---|---|
| Archivo | docs/02_jg_agent/articulos/07_ssrf_dns_pinning_tcp.md |
| Descripción | Por qué la validación sintáctica de URLs no protege contra Server-Side Request Forgery: pinning de sockets, normalización IPv6/IPv4 y mitigación de TOCTOU en red. |
| Autor | Javier Gálvez |
| Fecha | 2026-10-17 |
| Etiquetas |
#Artículos
#Arquitectura
#Seguridad
#SSRF
#DNS Rebinding
#DNS Pinning
#TCP
#AppSec
|