VYPR
High severityNVD Advisory· Published Mar 19, 2026· Updated Mar 20, 2026

OpenClaw < 2026.2.23 - Sandbox Bypass in apply_patch Tool via Workspace-Only Check Bypass

CVE-2026-32007

Description

OpenClaw versions prior to 2026.2.23 contain a path traversal vulnerability in the experimental apply_patch tool that allows attackers with sandbox access to modify files outside the workspace directory by exploiting inconsistent enforcement of workspace-only checks on mounted paths. Attackers can use apply_patch operations on writable mounts outside the workspace root to access and modify arbitrary files on the system.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
openclawnpm
< 2026.2.232026.2.23

Affected products

1

Patches

1
6634030be31e

fix: enforce apply_patch workspaceOnly in sandbox mounts

https://github.com/openclaw/openclawPeter SteinbergerFeb 24, 2026via ghsa
3 files changed · +83 0
  • CHANGELOG.md+1 0 modified
    @@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai
     
     - Security/Commands: enforce sender-only matching for `commands.allowFrom` by blocking conversation-shaped `From` identities (`channel:`, `group:`, `thread:`, `@g.us`) while preserving direct-message fallback when sender fields are missing. Ships in the next npm release. Thanks @jiseoung.
     - Config/Kilo Gateway: Kilo provider flow now surfaces an updated list of models. (#24921) thanks @gumadeiras.
    +- Security/Sandbox: enforce `tools.exec.applyPatch.workspaceOnly` and `tools.fs.workspaceOnly` for `apply_patch` in sandbox-mounted paths so writes/deletes cannot escape the workspace boundary via mounts like `/agent` unless explicitly opted out (`tools.exec.applyPatch.workspaceOnly=false`). This ships in the next npm release. Thanks @tdjackey for reporting.
     - Security/Config writes: block reserved prototype keys in account-id normalization and route account config resolution through own-key lookups, hardening `/allowlist` and account-scoped config paths against prototype-chain pollution.
     - Security/Exec: harden `safeBins` long-option validation by rejecting unknown/ambiguous GNU long-option abbreviations and denying sort filesystem-dependent flags (`--random-source`, `--temporary-directory`, `-T`), closing safe-bin denylist bypasses. Thanks @jiseoung.
     - Security/Channels: unify dangerous name-matching policy checks (`dangerouslyAllowNameMatching`) across core and extension channels, share mutable-allowlist detectors between `openclaw doctor` and `openclaw security audit`, and scan all configured accounts (not only the default account) in channel security audit findings.
    
  • src/agents/apply-patch.ts+8 0 modified
    @@ -260,6 +260,14 @@ async function resolvePatchPath(
           filePath,
           cwd: options.cwd,
         });
    +    if (options.workspaceOnly !== false) {
    +      await assertSandboxPath({
    +        filePath: resolved.hostPath,
    +        cwd: options.cwd,
    +        root: options.cwd,
    +        allowFinalSymlink: purpose === "unlink",
    +      });
    +    }
         return {
           resolved: resolved.hostPath,
           display: resolved.relativePath || resolved.hostPath,
    
  • src/agents/pi-tools.sandbox-mounted-paths.workspace-only.test.ts+74 0 modified
    @@ -73,6 +73,10 @@ function createSandbox(params: {
       });
     }
     
    +type ToolWithExecute = {
    +  execute: (toolCallId: string, args: unknown, signal?: AbortSignal) => Promise<unknown>;
    +};
    +
     async function withUnsafeMountedSandboxHarness(
       run: (ctx: { sandboxRoot: string; agentRoot: string; sandbox: SandboxContext }) => Promise<void>,
     ) {
    @@ -131,4 +135,74 @@ describe("tools.fs.workspaceOnly", () => {
           expect(await fs.readFile(path.join(agentRoot, "secret.txt"), "utf8")).toBe("shh");
         });
       });
    +
    +  it("enforces apply_patch workspace-only in sandbox mounts by default", async () => {
    +    await withUnsafeMountedSandboxHarness(async ({ sandboxRoot, agentRoot, sandbox }) => {
    +      const cfg: OpenClawConfig = {
    +        tools: {
    +          allow: ["read", "exec"],
    +          exec: { applyPatch: { enabled: true } },
    +        },
    +      };
    +      const tools = createOpenClawCodingTools({
    +        sandbox,
    +        workspaceDir: sandboxRoot,
    +        config: cfg,
    +        modelProvider: "openai",
    +        modelId: "gpt-5.2",
    +      });
    +      const applyPatchTool = tools.find((t) => t.name === "apply_patch") as
    +        | ToolWithExecute
    +        | undefined;
    +      if (!applyPatchTool) {
    +        throw new Error("apply_patch tool missing");
    +      }
    +
    +      const patch = `*** Begin Patch
    +*** Add File: /agent/pwned.txt
    ++owned-by-apply-patch
    +*** End Patch`;
    +
    +      await expect(applyPatchTool.execute("t1", { input: patch })).rejects.toThrow(
    +        /Path escapes sandbox root/i,
    +      );
    +      await expect(fs.stat(path.join(agentRoot, "pwned.txt"))).rejects.toMatchObject({
    +        code: "ENOENT",
    +      });
    +    });
    +  });
    +
    +  it("allows apply_patch outside workspace root when explicitly disabled", async () => {
    +    await withUnsafeMountedSandboxHarness(async ({ sandboxRoot, agentRoot, sandbox }) => {
    +      const cfg: OpenClawConfig = {
    +        tools: {
    +          allow: ["read", "exec"],
    +          exec: { applyPatch: { enabled: true, workspaceOnly: false } },
    +        },
    +      };
    +      const tools = createOpenClawCodingTools({
    +        sandbox,
    +        workspaceDir: sandboxRoot,
    +        config: cfg,
    +        modelProvider: "openai",
    +        modelId: "gpt-5.2",
    +      });
    +      const applyPatchTool = tools.find((t) => t.name === "apply_patch") as
    +        | ToolWithExecute
    +        | undefined;
    +      if (!applyPatchTool) {
    +        throw new Error("apply_patch tool missing");
    +      }
    +
    +      const patch = `*** Begin Patch
    +*** Add File: /agent/pwned.txt
    ++owned-by-apply-patch
    +*** End Patch`;
    +
    +      await applyPatchTool.execute("t2", { input: patch });
    +      expect(await fs.readFile(path.join(agentRoot, "pwned.txt"), "utf8")).toBe(
    +        "owned-by-apply-patch\n",
    +      );
    +    });
    +  });
     });
    

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.