High severity8.1NVD Advisory· Published Apr 28, 2026· Updated May 1, 2026
CVE-2026-41383
CVE-2026-41383
Description
OpenClaw before 2026.4.2 contains an arbitrary directory deletion vulnerability in mirror mode that allows attackers to delete remote directories by influencing remoteWorkspaceDir and remoteAgentWorkspaceDir configuration values. Attackers can manipulate these OpenShell config paths to cause mirror sync operations to delete unintended remote directory contents and replace them with uploaded workspace data.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
openclawnpm | < 2026.4.2 | 2026.4.2 |
Affected products
1Patches
1b21c9840c2e3OpenShell: constrain mirror sync roots (#58515)
3 files changed · +103 −52
extensions/openshell/src/config.test.ts+72 −0 added@@ -0,0 +1,72 @@ +import fsSync from "node:fs"; +import { describe, expect, it } from "vitest"; +import { createOpenShellPluginConfigSchema, resolveOpenShellPluginConfig } from "./config.js"; + +describe("openshell plugin config", () => { + it("applies defaults", () => { + expect(resolveOpenShellPluginConfig(undefined)).toEqual({ + mode: "mirror", + command: "openshell", + gateway: undefined, + gatewayEndpoint: undefined, + from: "openclaw", + policy: undefined, + providers: [], + gpu: false, + autoProviders: true, + remoteWorkspaceDir: "/sandbox", + remoteAgentWorkspaceDir: "/agent", + timeoutMs: 120_000, + }); + }); + + it("accepts remote mode", () => { + expect(resolveOpenShellPluginConfig({ mode: "remote" }).mode).toBe("remote"); + }); + + it("rejects relative remote paths", () => { + expect(() => + resolveOpenShellPluginConfig({ + remoteWorkspaceDir: "sandbox", + }), + ).toThrow("OpenShell remoteWorkspaceDir must be absolute"); + }); + + it("rejects remote paths outside managed sandbox roots", () => { + expect(() => + resolveOpenShellPluginConfig({ + remoteWorkspaceDir: "/tmp/victim", + }), + ).toThrow("OpenShell remoteWorkspaceDir must stay under /sandbox or /agent"); + }); + + it("normalizes managed sandbox subpaths", () => { + expect( + resolveOpenShellPluginConfig({ + remoteWorkspaceDir: "/sandbox/../sandbox/project", + remoteAgentWorkspaceDir: "/agent/./session", + }), + ).toEqual( + expect.objectContaining({ + remoteWorkspaceDir: "/sandbox/project", + remoteAgentWorkspaceDir: "/agent/session", + }), + ); + }); + + it("rejects unknown mode", () => { + expect(() => + resolveOpenShellPluginConfig({ + mode: "bogus", + }), + ).toThrow("mode must be one of mirror, remote"); + }); + + it("keeps the runtime json schema in sync with the manifest config schema", () => { + const manifest = JSON.parse( + fsSync.readFileSync(new URL("../openclaw.plugin.json", import.meta.url), "utf8"), + ) as { configSchema?: unknown }; + + expect(createOpenShellPluginConfigSchema().jsonSchema).toEqual(manifest.configSchema); + }); +});
extensions/openshell/src/config.ts+30 −4 modified@@ -38,6 +38,10 @@ const DEFAULT_SOURCE = "openclaw"; const DEFAULT_REMOTE_WORKSPACE_DIR = "/sandbox"; const DEFAULT_REMOTE_AGENT_WORKSPACE_DIR = "/agent"; const DEFAULT_TIMEOUT_MS = 120_000; +const OPEN_SHELL_MANAGED_REMOTE_ROOTS = [ + DEFAULT_REMOTE_WORKSPACE_DIR, + DEFAULT_REMOTE_AGENT_WORKSPACE_DIR, +] as const; function normalizeProviders(value: string[] | undefined): string[] { const seen = new Set<string>(); @@ -100,11 +104,26 @@ function formatOpenShellConfigIssue(issue: z.ZodIssue | undefined): string { return issue.message; } -function normalizeRemotePath(value: string | undefined, fallback: string): string { +function isManagedOpenShellRemotePath(value: string): boolean { + return OPEN_SHELL_MANAGED_REMOTE_ROOTS.some( + (root) => value === root || value.startsWith(`${root}/`), + ); +} + +export function normalizeOpenShellRemotePath( + value: string | undefined, + fallback: string, + fieldName = "remote path", +): string { const candidate = value ?? fallback; const normalized = path.posix.normalize(candidate.trim() || fallback); if (!normalized.startsWith("/")) { - throw new Error(`OpenShell remote path must be absolute: ${candidate}`); + throw new Error(`OpenShell ${fieldName} must be absolute: ${candidate}`); + } + if (!isManagedOpenShellRemotePath(normalized)) { + throw new Error( + `OpenShell ${fieldName} must stay under ${OPEN_SHELL_MANAGED_REMOTE_ROOTS.join(" or ")}: ${candidate}`, + ); } return normalized; } @@ -137,6 +156,8 @@ export function createOpenShellPluginConfigSchema(): OpenClawPluginConfigSchema export function resolveOpenShellPluginConfig(value: unknown): ResolvedOpenShellPluginConfig { if (value === undefined) { + // The built-in defaults are managed OpenShell roots, so they do not need to + // flow back through normalizeOpenShellRemotePath. return { mode: DEFAULT_MODE, command: DEFAULT_COMMAND, @@ -170,10 +191,15 @@ export function resolveOpenShellPluginConfig(value: unknown): ResolvedOpenShellP providers: normalizeProviders(cfg.providers), gpu: cfg.gpu ?? false, autoProviders: cfg.autoProviders ?? true, - remoteWorkspaceDir: normalizeRemotePath(cfg.remoteWorkspaceDir, DEFAULT_REMOTE_WORKSPACE_DIR), - remoteAgentWorkspaceDir: normalizeRemotePath( + remoteWorkspaceDir: normalizeOpenShellRemotePath( + cfg.remoteWorkspaceDir, + DEFAULT_REMOTE_WORKSPACE_DIR, + "remoteWorkspaceDir", + ), + remoteAgentWorkspaceDir: normalizeOpenShellRemotePath( cfg.remoteAgentWorkspaceDir, DEFAULT_REMOTE_AGENT_WORKSPACE_DIR, + "remoteAgentWorkspaceDir", ), timeoutMs: typeof cfg.timeoutSeconds === "number"
extensions/openshell/src/openshell-core.test.ts+1 −48 modified@@ -12,61 +12,14 @@ import { setBundledOpenShellCommandResolverForTest, shellEscape, } from "./cli.js"; -import { createOpenShellPluginConfigSchema, resolveOpenShellPluginConfig } from "./config.js"; +import { resolveOpenShellPluginConfig } from "./config.js"; const cliMocks = vi.hoisted(() => ({ runOpenShellCli: vi.fn(), })); let createOpenShellSandboxBackendManager: typeof import("./backend.js").createOpenShellSandboxBackendManager; -describe("openshell plugin config", () => { - it("applies defaults", () => { - expect(resolveOpenShellPluginConfig(undefined)).toEqual({ - mode: "mirror", - command: "openshell", - gateway: undefined, - gatewayEndpoint: undefined, - from: "openclaw", - policy: undefined, - providers: [], - gpu: false, - autoProviders: true, - remoteWorkspaceDir: "/sandbox", - remoteAgentWorkspaceDir: "/agent", - timeoutMs: 120_000, - }); - }); - - it("accepts remote mode", () => { - expect(resolveOpenShellPluginConfig({ mode: "remote" }).mode).toBe("remote"); - }); - - it("rejects relative remote paths", () => { - expect(() => - resolveOpenShellPluginConfig({ - remoteWorkspaceDir: "sandbox", - }), - ).toThrow("OpenShell remote path must be absolute"); - }); - - it("rejects unknown mode", () => { - expect(() => - resolveOpenShellPluginConfig({ - mode: "bogus", - }), - ).toThrow("mode must be one of mirror, remote"); - }); - - it("keeps the runtime json schema in sync with the manifest config schema", () => { - const manifest = JSON.parse( - fsSync.readFileSync(new URL("../openclaw.plugin.json", import.meta.url), "utf8"), - ) as { configSchema?: unknown }; - - expect(createOpenShellPluginConfigSchema().jsonSchema).toEqual(manifest.configSchema); - }); -}); - describe("openshell cli helpers", () => { afterEach(() => { setBundledOpenShellCommandResolverForTest();
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
4- github.com/openclaw/openclaw/commit/b21c9840c2e38f4bb338d031511b479d5f07ca25nvdPatchWEB
- github.com/advisories/GHSA-m34q-h93w-vg5xghsaADVISORY
- github.com/openclaw/openclaw/security/advisories/GHSA-m34q-h93w-vg5xnvdVendor AdvisoryWEB
- www.vulncheck.com/advisories/openclaw-arbitrary-remote-directory-deletion-via-mis-scoped-mirror-mode-pathsnvdThird Party Advisory
News mentions
0No linked articles in our index yet.