VYPR
Moderate severityNVD Advisory· Published Mar 19, 2026· Updated Mar 25, 2026

OpenClaw < 2026.2.24 - Path Traversal via @-prefixed Absolute Paths in Workspace Boundary Validation

CVE-2026-32033

Description

OpenClaw versions prior to 2026.2.24 contain a path traversal vulnerability where @-prefixed absolute paths bypass workspace-only file-system boundary validation due to canonicalization mismatch. Attackers can exploit this by crafting @-prefixed paths like @/etc/passwd to read files outside the intended workspace boundary when tools.fs.workspaceOnly is enabled.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
openclawnpm
< 2026.2.242026.2.24

Affected products

1

Patches

1
9ef0fc2ff8fa

fix(sandbox): block @-prefixed workspace path bypass

https://github.com/openclaw/openclawPeter SteinbergerFeb 24, 2026via ghsa
6 files changed · +58 3
  • CHANGELOG.md+1 0 modified
    @@ -11,6 +11,7 @@ Docs: https://docs.openclaw.ai
     
     ### Fixes
     
    +- Security/Workspace FS: normalize `@`-prefixed paths before workspace-boundary checks (including workspace-only read/write/edit and sandbox mount path guards), preventing absolute-path escape attempts from bypassing guard validation. This ships in the next npm release. Thanks @tdjackey for reporting.
     - Security/Native images: enforce `tools.fs.workspaceOnly` for native prompt image auto-load (including history refs), preventing out-of-workspace sandbox mounts from being implicitly ingested as vision input. This ships in the next npm release. Thanks @tdjackey for reporting.
     - Security/Exec approvals: bind `system.run` command display/approval text to full argv when shell-wrapper inline payloads carry positional argv values, and reject payload-only `rawCommand` mismatches for those wrapper-carrier forms, preventing hidden command execution under misleading approval text. This ships in the next npm release. Thanks @tdjackey for reporting.
     - Telegram/Media fetch: prioritize IPv4 before IPv6 in SSRF pinned DNS address ordering so media downloads still work on hosts with broken IPv6 routing. (#24295, #23975) Thanks @Glucksberg.
    
  • src/agents/pi-tools.read.ts+1 1 modified
    @@ -571,7 +571,7 @@ function mapContainerPathToWorkspaceRoot(params: {
         return params.filePath;
       }
     
    -  let candidate = params.filePath;
    +  let candidate = params.filePath.startsWith("@") ? params.filePath.slice(1) : params.filePath;
       if (/^file:\/\//i.test(candidate)) {
         try {
           candidate = fileURLToPath(candidate);
    
  • src/agents/pi-tools.read.workspace-root-guard.test.ts+30 0 modified
    @@ -61,6 +61,36 @@ describe("wrapToolWorkspaceRootGuardWithOptions", () => {
         });
       });
     
    +  it("maps @-prefixed container workspace paths to host workspace root", async () => {
    +    const { tool } = createToolHarness();
    +    const wrapped = wrapToolWorkspaceRootGuardWithOptions(tool, root, {
    +      containerWorkdir: "/workspace",
    +    });
    +
    +    await wrapped.execute("tc-at-container", { path: "@/workspace/docs/readme.md" });
    +
    +    expect(mocks.assertSandboxPath).toHaveBeenCalledWith({
    +      filePath: path.resolve(root, "docs", "readme.md"),
    +      cwd: root,
    +      root,
    +    });
    +  });
    +
    +  it("normalizes @-prefixed absolute paths before guard checks", async () => {
    +    const { tool } = createToolHarness();
    +    const wrapped = wrapToolWorkspaceRootGuardWithOptions(tool, root, {
    +      containerWorkdir: "/workspace",
    +    });
    +
    +    await wrapped.execute("tc-at-absolute", { path: "@/etc/passwd" });
    +
    +    expect(mocks.assertSandboxPath).toHaveBeenCalledWith({
    +      filePath: "/etc/passwd",
    +      cwd: root,
    +      root,
    +    });
    +  });
    +
       it("does not remap absolute paths outside the configured container workdir", async () => {
         const { tool } = createToolHarness();
         const wrapped = wrapToolWorkspaceRootGuardWithOptions(tool, root, {
    
  • src/agents/pi-tools.workspace-paths.test.ts+14 0 modified
    @@ -2,6 +2,7 @@ import fs from "node:fs/promises";
     import os from "node:os";
     import path from "node:path";
     import { describe, expect, it, vi } from "vitest";
    +import type { OpenClawConfig } from "../config/config.js";
     import { createOpenClawCodingTools } from "./pi-tools.js";
     import { createHostSandboxFsBridge } from "./test-helpers/host-sandbox-fs-bridge.js";
     import { expectReadWriteEditTools, getTextContent } from "./test-helpers/pi-tools-fs-helpers.js";
    @@ -137,6 +138,19 @@ describe("workspace path resolution", () => {
           });
         });
       });
    +
    +  it("rejects @-prefixed absolute paths outside workspace when workspaceOnly is enabled", async () => {
    +    await withTempDir("openclaw-ws-", async (workspaceDir) => {
    +      const cfg: OpenClawConfig = { tools: { fs: { workspaceOnly: true } } };
    +      const tools = createOpenClawCodingTools({ workspaceDir, config: cfg });
    +      const { readTool } = expectReadWriteEditTools(tools);
    +
    +      const outsideAbsolute = path.resolve(path.parse(workspaceDir).root, "outside-openclaw.txt");
    +      await expect(
    +        readTool.execute("ws-read-at-prefix", { path: `@${outsideAbsolute}` }),
    +      ).rejects.toThrow(/Path escapes sandbox root/i);
    +    });
    +  });
     });
     
     describe("sandboxed workspace paths", () => {
    
  • src/agents/sandbox/fs-paths.ts+7 1 modified
    @@ -227,7 +227,13 @@ function isPathInsidePosix(root: string, target: string): boolean {
     
     function isPathInsideHost(root: string, target: string): boolean {
       const canonicalRoot = resolveSandboxHostPathViaExistingAncestor(path.resolve(root));
    -  const canonicalTarget = resolveSandboxHostPathViaExistingAncestor(path.resolve(target));
    +  const resolvedTarget = path.resolve(target);
    +  // Preserve the final path segment so pre-existing symlink leaves are validated
    +  // by the dedicated symlink guard later in the bridge flow.
    +  const canonicalTargetParent = resolveSandboxHostPathViaExistingAncestor(
    +    path.dirname(resolvedTarget),
    +  );
    +  const canonicalTarget = path.resolve(canonicalTargetParent, path.basename(resolvedTarget));
       const rel = path.relative(canonicalRoot, canonicalTarget);
       if (!rel) {
         return true;
    
  • src/agents/sandbox-paths.ts+5 1 modified
    @@ -13,8 +13,12 @@ function normalizeUnicodeSpaces(str: string): string {
       return str.replace(UNICODE_SPACES, " ");
     }
     
    +function normalizeAtPrefix(filePath: string): string {
    +  return filePath.startsWith("@") ? filePath.slice(1) : filePath;
    +}
    +
     function expandPath(filePath: string): string {
    -  const normalized = normalizeUnicodeSpaces(filePath);
    +  const normalized = normalizeUnicodeSpaces(normalizeAtPrefix(filePath));
       if (normalized === "~") {
         return os.homedir();
       }
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.