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.
| Package | Affected versions | Patched versions |
|---|---|---|
openclawnpm | < 2026.4.22 | 2026.4.22 |
Affected products
2Patches
17be82d4fd119fix(openshell): pin host writes to sandbox root (#69797)
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- github.com/openclaw/openclaw/commit/7be82d4fd1193bcb7e44ee38838f00bf924ffa76nvdPatchWEB
- github.com/advisories/GHSA-wppj-c6mr-83jjghsaADVISORY
- github.com/openclaw/openclaw/security/advisories/GHSA-wppj-c6mr-83jjnvdMitigationVendor AdvisoryWEB
- nvd.nist.gov/vuln/detail/CVE-2026-44112ghsaADVISORY
- www.vulncheck.com/advisories/openclaw-symlink-swap-race-condition-in-openshell-fs-bridge-writesnvdThird Party AdvisoryWEB
News mentions
1- Four OpenClaw Flaws Enable Data Theft, Privilege Escalation, and PersistenceThe Hacker News · May 15, 2026