VYPR
High severity7.3NVD Advisory· Published Apr 28, 2026· Updated Apr 30, 2026

CVE-2026-41390

CVE-2026-41390

Description

OpenClaw before 2026.3.28 contains an exec allowlist bypass vulnerability where allow-always persistence fails to unwrap /usr/bin/script and similar wrappers before storing trust decisions. Attackers can obtain user approval for one wrapped command to persist trust for wrapper binaries that execute different underlying programs.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
openclawnpm
< 2026.3.282026.3.28

Affected products

1
  • cpe:2.3:a:openclaw:openclaw:*:*:*:*:*:node.js:*:*
    Range: <2026.3.28

Patches

1
83da3cfe31f0

infra: unwrap script wrapper approval targets (#55685)

https://github.com/openclaw/openclawJacob TomlinsonMar 27, 2026via ghsa
4 files changed · +96 0
  • src/infra/dispatch-wrapper-resolution.ts+44 0 modified
    @@ -42,6 +42,8 @@ const TIME_FLAG_OPTIONS = new Set([
       "--version",
     ]);
     const TIME_OPTIONS_WITH_VALUE = new Set(["-f", "--format", "-o", "--output"]);
    +const BSD_SCRIPT_FLAG_OPTIONS = new Set(["-a", "-d", "-k", "-p", "-q", "-r"]);
    +const BSD_SCRIPT_OPTIONS_WITH_VALUE = new Set(["-F", "-t"]);
     const TIMEOUT_FLAG_OPTIONS = new Set(["--foreground", "--preserve-status", "-v", "--verbose"]);
     const TIMEOUT_OPTIONS_WITH_VALUE = new Set(["-k", "--kill-after", "-s", "--signal"]);
     
    @@ -259,6 +261,47 @@ function unwrapTimeInvocation(argv: string[]): string[] | null {
       });
     }
     
    +function supportsScriptPositionalCommand(platform: NodeJS.Platform = process.platform): boolean {
    +  return platform === "darwin" || platform === "freebsd";
    +}
    +
    +function unwrapScriptInvocation(argv: string[]): string[] | null {
    +  if (!supportsScriptPositionalCommand()) {
    +    return null;
    +  }
    +  return scanWrapperInvocation(argv, {
    +    separators: new Set(["--"]),
    +    onToken: (token, lower) => {
    +      if (!lower.startsWith("-") || lower === "-") {
    +        return "stop";
    +      }
    +      const [flag] = token.split("=", 2);
    +      if (BSD_SCRIPT_OPTIONS_WITH_VALUE.has(flag)) {
    +        return token.includes("=") ? "continue" : "consume-next";
    +      }
    +      if (BSD_SCRIPT_FLAG_OPTIONS.has(flag)) {
    +        return "continue";
    +      }
    +      return "invalid";
    +    },
    +    adjustCommandIndex: (commandIndex, currentArgv) => {
    +      let sawTranscript = false;
    +      for (let idx = commandIndex; idx < currentArgv.length; idx += 1) {
    +        const token = currentArgv[idx]?.trim() ?? "";
    +        if (!token) {
    +          continue;
    +        }
    +        if (!sawTranscript) {
    +          sawTranscript = true;
    +          continue;
    +        }
    +        return idx;
    +      }
    +      return null;
    +    },
    +  });
    +}
    +
     function unwrapTimeoutInvocation(argv: string[]): string[] | null {
       return unwrapDashOptionInvocation(argv, {
         onFlag: (flag, lower) => {
    @@ -294,6 +337,7 @@ const DISPATCH_WRAPPER_SPECS: readonly DispatchWrapperSpec[] = [
       { name: "ionice" },
       { name: "nice", unwrap: unwrapNiceInvocation, transparentUsage: true },
       { name: "nohup", unwrap: unwrapNohupInvocation, transparentUsage: true },
    +  { name: "script", unwrap: unwrapScriptInvocation, transparentUsage: true },
       { name: "setsid" },
       { name: "stdbuf", unwrap: unwrapStdbufInvocation, transparentUsage: true },
       { name: "sudo" },
    
  • src/infra/exec-approvals-allow-always.test.ts+17 0 modified
    @@ -619,6 +619,23 @@ $0 \\"$1\\"" touch {marker}`,
         });
       });
     
    +  it("prevents allow-always bypass for script wrapper chains", () => {
    +    if (process.platform !== "darwin" && process.platform !== "freebsd") {
    +      return;
    +    }
    +    const dir = makeTempDir();
    +    const echo = makeExecutable(dir, "echo");
    +    makeExecutable(dir, "id");
    +    const env = makePathEnv(dir);
    +    expectAllowAlwaysBypassBlocked({
    +      dir,
    +      firstCommand: "/usr/bin/script -q /dev/null /bin/sh -lc 'echo warmup-ok'",
    +      secondCommand: "/usr/bin/script -q /dev/null /bin/sh -lc 'id > marker'",
    +      env,
    +      persistedPattern: echo,
    +    });
    +  });
    +
       it("does not persist comment-tailed payload paths that never execute", () => {
         if (process.platform === "win32") {
           return;
    
  • src/infra/exec-wrapper-resolution.test.ts+19 0 modified
    @@ -13,6 +13,10 @@ import {
       unwrapKnownShellMultiplexerInvocation,
     } from "./exec-wrapper-resolution.js";
     
    +function supportsScriptPositionalCommandForTests(): boolean {
    +  return process.platform === "darwin" || process.platform === "freebsd";
    +}
    +
     describe("basenameLower", () => {
       test.each([
         { token: " Bun.CMD ", expected: "bun.cmd" },
    @@ -40,6 +44,7 @@ describe("normalizeExecutableToken", () => {
     describe("wrapper classification", () => {
       test.each([
         { token: "sudo", dispatch: true, shell: false },
    +    { token: "script", dispatch: true, shell: false },
         { token: "time", dispatch: true, shell: false },
         { token: "timeout.exe", dispatch: true, shell: false },
         { token: "bash", dispatch: false, shell: true },
    @@ -119,6 +124,16 @@ describe("unwrapKnownDispatchWrapperInvocation", () => {
           argv: ["nohup", "--", "bash", "-lc", "echo hi"],
           expected: { kind: "unwrapped", wrapper: "nohup", argv: ["bash", "-lc", "echo hi"] },
         },
    +    {
    +      argv: ["script", "-q", "/dev/null", "bash", "-lc", "echo hi"],
    +      expected: supportsScriptPositionalCommandForTests()
    +        ? { kind: "unwrapped", wrapper: "script", argv: ["bash", "-lc", "echo hi"] }
    +        : { kind: "blocked", wrapper: "script" },
    +    },
    +    {
    +      argv: ["script", "-E", "always", "/dev/null", "bash", "-lc", "echo hi"],
    +      expected: { kind: "blocked", wrapper: "script" },
    +    },
         {
           argv: ["stdbuf", "-o", "L", "bash", "-lc", "echo hi"],
           expected: { kind: "unwrapped", wrapper: "stdbuf", argv: ["bash", "-lc", "echo hi"] },
    @@ -131,6 +146,10 @@ describe("unwrapKnownDispatchWrapperInvocation", () => {
           argv: ["timeout", "--signal=TERM", "5s", "bash", "-lc", "echo hi"],
           expected: { kind: "unwrapped", wrapper: "timeout", argv: ["bash", "-lc", "echo hi"] },
         },
    +    {
    +      argv: ["script", "-q", "/dev/null"],
    +      expected: { kind: "blocked", wrapper: "script" },
    +    },
         {
           argv: ["sudo", "bash", "-lc", "echo hi"],
           expected: { kind: "blocked", wrapper: "sudo" },
    
  • src/infra/exec-wrapper-trust-plan.test.ts+16 0 modified
    @@ -18,6 +18,22 @@ describe("resolveExecWrapperTrustPlan", () => {
         });
       });
     
    +  test("unwraps script wrappers before evaluating nested shell payloads", () => {
    +    if (process.platform !== "darwin" && process.platform !== "freebsd") {
    +      return;
    +    }
    +    expect(
    +      resolveExecWrapperTrustPlan(["/usr/bin/script", "-q", "/dev/null", "sh", "-lc", "echo hi"]),
    +    ).toEqual({
    +      argv: ["sh", "-lc", "echo hi"],
    +      policyArgv: ["sh", "-lc", "echo hi"],
    +      wrapperChain: ["script"],
    +      policyBlocked: false,
    +      shellWrapperExecutable: true,
    +      shellInlineCommand: "echo hi",
    +    });
    +  });
    +
       test("fails closed for unsupported shell multiplexer applets", () => {
         expect(resolveExecWrapperTrustPlan(["busybox", "sed", "-n", "1p"])).toEqual({
           argv: ["busybox", "sed", "-n", "1p"],
    

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.