VYPR
Critical severity9.6NVD Advisory· Published May 6, 2026· Updated May 13, 2026

CVE-2026-44112

CVE-2026-44112

Description

OpenClaw before 2026.4.22 contains a time-of-check/time-of-use race condition in OpenShell sandbox filesystem writes that allows attackers to redirect writes outside the intended mount root. Attackers can exploit symlink swaps during filesystem operations to bypass sandbox restrictions and write files outside the local mount root.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
openclawnpm
< 2026.4.222026.4.22

Affected products

2
  • OpenClaw/Openclawreferences2 versions
    (expand)+ 1 more
    • (no CPE)
    • cpe:2.3:a:openclaw:openclaw:*:*:*:*:*:node.js:*:*range: <2026.4.22

Patches

1
7be82d4fd119

fix(openshell): pin host writes to sandbox root (#69797)

https://github.com/openclaw/openclawDevin RobisonApr 21, 2026via ghsa
3 files changed · +67 10
  • CHANGELOG.md+1 0 modified
    @@ -31,6 +31,7 @@ Docs: https://docs.openclaw.ai
     - Exec/allowlist: reject POSIX parameter expansion forms such as `$VAR`, `$?`, `$$`, `$1`, and `$@` inside unquoted heredocs during shell approval analysis, so these heredocs no longer pass allowlist review as plain text. (#69795) Thanks @drobison00.
     - Gateway/MCP loopback: derive owner-only tool visibility from distinct authenticated owner vs non-owner loopback bearers instead of the caller-controlled owner header, so non-owner MCP child processes cannot recover owner access by spoofing request metadata. (#69796)
     - GitHub Copilot: update the default Opus model from `claude-opus-4.6` to `claude-opus-4.7` after GitHub removed Copilot support for 4.6. (#69818) Thanks @shakkernerd.
    +- OpenShell: pin host-side sandbox writes under the mounted root so symlink-parent rebinds cannot redirect `writeFile` outside the workspace during local mirror updates. (#69797) Thanks @drobison00.
     
     ## 2026.4.20
     
    
  • extensions/openshell/src/fs-bridge.ts+7 10 modified
    @@ -6,6 +6,7 @@ import type {
       SandboxResolvedPath,
     } from "openclaw/plugin-sdk/sandbox";
     import { createWritableRenameTargetResolver } from "openclaw/plugin-sdk/sandbox";
    +import { writeFileWithinRoot } from "openclaw/plugin-sdk/infra-runtime";
     import type { OpenShellFsBridgeContext, OpenShellSandboxBackend } from "./backend.types.js";
     import { movePathWithCopyFallback } from "./mirror.js";
     
    @@ -78,16 +79,12 @@ class OpenShellFsBridge implements SandboxFsBridge {
         const buffer = Buffer.isBuffer(params.data)
           ? params.data
           : Buffer.from(params.data, params.encoding ?? "utf8");
    -    const parentDir = path.dirname(hostPath);
    -    if (params.mkdir !== false) {
    -      await fsPromises.mkdir(parentDir, { recursive: true });
    -    }
    -    const tempPath = path.join(
    -      parentDir,
    -      `.openclaw-openshell-write-${path.basename(hostPath)}-${process.pid}-${Date.now()}`,
    -    );
    -    await fsPromises.writeFile(tempPath, buffer);
    -    await fsPromises.rename(tempPath, hostPath);
    +    await writeFileWithinRoot({
    +      rootDir: target.mountHostRoot,
    +      relativePath: path.relative(target.mountHostRoot, hostPath),
    +      data: buffer,
    +      mkdir: params.mkdir,
    +    });
         await this.backend.syncLocalPathToRemote(hostPath, target.containerPath);
       }
     
    
  • extensions/openshell/src/openshell-core.test.ts+59 0 modified
    @@ -248,6 +248,65 @@ describe("openshell fs bridges", () => {
         );
       });
     
    +  it("rejects symlink-parent writes instead of escaping the local mount root", async () => {
    +    const workspaceDir = await makeTempDir("openclaw-openshell-fs-");
    +    const outsideDir = await makeTempDir("openclaw-openshell-outside-");
    +    await fs.symlink(outsideDir, path.join(workspaceDir, "alias"));
    +    const backend = createMirrorBackendMock();
    +    const sandbox = createSandboxTestContext({
    +      overrides: {
    +        backendId: "openshell",
    +        workspaceDir,
    +        agentWorkspaceDir: workspaceDir,
    +        containerWorkdir: "/sandbox",
    +      },
    +    });
    +
    +    const { createOpenShellFsBridge } = await import("./fs-bridge.js");
    +    const bridge = createOpenShellFsBridge({ sandbox, backend });
    +
    +    await expect(
    +      bridge.writeFile({
    +        filePath: "alias/escape.txt",
    +        data: "owned",
    +        mkdir: true,
    +      }),
    +    ).rejects.toThrow();
    +    await expect(fs.stat(path.join(outsideDir, "escape.txt"))).rejects.toThrow();
    +    await expect(fs.readdir(outsideDir)).resolves.toEqual([]);
    +    expect(backend.syncLocalPathToRemote).not.toHaveBeenCalled();
    +  });
    +
    +  it("rejects writes whose final target is a symlink inside the local mount root", async () => {
    +    const workspaceDir = await makeTempDir("openclaw-openshell-fs-");
    +    const linkedTarget = path.join(workspaceDir, "existing.txt");
    +    await fs.writeFile(linkedTarget, "keep", "utf8");
    +    await fs.symlink("existing.txt", path.join(workspaceDir, "link.txt"));
    +    const backend = createMirrorBackendMock();
    +    const sandbox = createSandboxTestContext({
    +      overrides: {
    +        backendId: "openshell",
    +        workspaceDir,
    +        agentWorkspaceDir: workspaceDir,
    +        containerWorkdir: "/sandbox",
    +      },
    +    });
    +
    +    const { createOpenShellFsBridge } = await import("./fs-bridge.js");
    +    const bridge = createOpenShellFsBridge({ sandbox, backend });
    +
    +    await expect(
    +      bridge.writeFile({
    +        filePath: "link.txt",
    +        data: "owned",
    +        mkdir: true,
    +      }),
    +    ).rejects.toThrow();
    +    await expect(fs.readlink(path.join(workspaceDir, "link.txt"))).resolves.toBe("existing.txt");
    +    await expect(fs.readFile(linkedTarget, "utf8")).resolves.toBe("keep");
    +    expect(backend.syncLocalPathToRemote).not.toHaveBeenCalled();
    +  });
    +
       it("maps agent mount paths when the sandbox workspace is read-only", async () => {
         const workspaceDir = await makeTempDir("openclaw-openshell-fs-");
         const agentWorkspaceDir = await makeTempDir("openclaw-openshell-agent-");
    

Vulnerability mechanics

AI mechanics synthesis has not run for this CVE yet.

References

5

News mentions

1