High severityNVD Advisory· Published Mar 18, 2026· Updated Mar 25, 2026
OpenClaw 2026.2.22 < 2026.2.23 - Arbitrary Binary Execution via $SHELL Environment Variable Trusted Prefix Fallback
CVE-2026-22217
Description
OpenClaw version 2026.2.22 prior to 2026.2.23 contain an arbitrary code execution vulnerability in shell-env that allows attackers to execute attacker-controlled binaries by exploiting trusted-prefix fallback logic for the $SHELL variable. An attacker can influence the $SHELL environment variable on systems with writable trusted-prefix directories such as /opt/homebrew/bin to execute arbitrary binaries in the OpenClaw process context.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
openclawnpm | >= 2026.2.22, < 2026.2.23 | 2026.2.23 |
Affected products
1Patches
1ff10fe8b9167fix(security): require /etc/shells for shell env fallback
2 files changed · +33 −27
src/infra/shell-env.test.ts+32 −5 modified@@ -28,6 +28,7 @@ describe("shell env fallback", () => { } function runShellEnvFallbackForShell(shell: string) { + resetShellPathCacheForTests(); const env: NodeJS.ProcessEnv = { SHELL: shell }; const exec = vi.fn(() => Buffer.from("OPENAI_API_KEY=from-shell\0")); const res = loadShellEnvFallback({ @@ -170,18 +171,44 @@ describe("shell env fallback", () => { expect(exec).toHaveBeenCalledWith("/bin/sh", ["-l", "-c", "env -0"], expect.any(Object)); }); - it("uses trusted absolute SHELL path when executable on posix-style paths", () => { - const accessSyncSpy = vi.spyOn(fs, "accessSync").mockImplementation(() => undefined); + it("falls back to /bin/sh when SHELL is absolute but not registered in /etc/shells", () => { + const readFileSyncSpy = vi + .spyOn(fs, "readFileSync") + .mockImplementation((filePath, encoding) => { + if (filePath === "/etc/shells" && encoding === "utf8") { + return "/bin/sh\n/bin/bash\n/bin/zsh\n"; + } + throw new Error(`Unexpected readFileSync(${String(filePath)}) in test`); + }); + try { + const { res, exec } = runShellEnvFallbackForShell("/opt/homebrew/bin/evil-shell"); + + expect(res.ok).toBe(true); + expect(exec).toHaveBeenCalledTimes(1); + expect(exec).toHaveBeenCalledWith("/bin/sh", ["-l", "-c", "env -0"], expect.any(Object)); + } finally { + readFileSyncSpy.mockRestore(); + } + }); + + it("uses SHELL when it is explicitly registered in /etc/shells", () => { + const readFileSyncSpy = vi + .spyOn(fs, "readFileSync") + .mockImplementation((filePath, encoding) => { + if (filePath === "/etc/shells" && encoding === "utf8") { + return "/bin/sh\n/usr/bin/zsh-trusted\n"; + } + throw new Error(`Unexpected readFileSync(${String(filePath)}) in test`); + }); try { const trustedShell = "/usr/bin/zsh-trusted"; const { res, exec } = runShellEnvFallbackForShell(trustedShell); - const expectedShell = process.platform === "win32" ? "/bin/sh" : trustedShell; expect(res.ok).toBe(true); expect(exec).toHaveBeenCalledTimes(1); - expect(exec).toHaveBeenCalledWith(expectedShell, ["-l", "-c", "env -0"], expect.any(Object)); + expect(exec).toHaveBeenCalledWith(trustedShell, ["-l", "-c", "env -0"], expect.any(Object)); } finally { - accessSyncSpy.mockRestore(); + readFileSyncSpy.mockRestore(); } });
src/infra/shell-env.ts+1 −22 modified@@ -8,13 +8,6 @@ import { sanitizeHostExecEnv } from "./host-env-security.js"; const DEFAULT_TIMEOUT_MS = 15_000; const DEFAULT_MAX_BUFFER_BYTES = 2 * 1024 * 1024; const DEFAULT_SHELL = "/bin/sh"; -const TRUSTED_SHELL_PREFIXES = [ - "/bin/", - "/usr/bin/", - "/usr/local/bin/", - "/opt/homebrew/bin/", - "/run/current-system/sw/bin/", -]; let lastAppliedKeys: string[] = []; let cachedShellPath: string | null | undefined; let cachedEtcShells: Set<string> | null | undefined; @@ -70,21 +63,7 @@ function isTrustedShellPath(shell: string): boolean { // Primary trust anchor: shell registered in /etc/shells. const registeredShells = readEtcShells(); - if (registeredShells?.has(shell)) { - return true; - } - - // Fallback for environments where /etc/shells is incomplete/unavailable. - if (!TRUSTED_SHELL_PREFIXES.some((prefix) => shell.startsWith(prefix))) { - return false; - } - - try { - fs.accessSync(shell, fs.constants.X_OK); - return true; - } catch { - return false; - } + return registeredShells?.has(shell) === true; } function resolveShell(env: NodeJS.ProcessEnv): string {
Vulnerability mechanics
Synthesis attempt was rejected by the grounding validator. Re-run pending.
References
5- github.com/openclaw/openclaw/commit/ff10fe8b91670044a6bb0cd85deb736a0ec8fb55ghsapatchWEB
- github.com/advisories/GHSA-p4wh-cr8m-gm6cghsaADVISORY
- github.com/openclaw/openclaw/security/advisories/GHSA-p4wh-cr8m-gm6cghsathird-party-advisoryWEB
- nvd.nist.gov/vuln/detail/CVE-2026-22217ghsaADVISORY
- www.vulncheck.com/advisories/openclaw-arbitrary-binary-execution-via-shell-environment-variable-trusted-prefix-fallbackghsathird-party-advisoryWEB
News mentions
0No linked articles in our index yet.