diff --git a/CHANGELOG.md b/CHANGELOG.md index 45caa8c6ce4..3c05dc83a34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Exec approvals: detect `env -S` split-string command-carrier risks when `-S`/`-s` is combined with other env short options, so approval explanations do not miss split payloads hidden behind `env -iS...`. Thanks @vincentkoc. - Voice Call: mark realtime calls completed when the realtime provider closes normally, so Twilio/OpenAI/Google realtime stop events do not leave active call records behind. Thanks @vincentkoc. - Exec approvals: treat POSIX `exec` as a command carrier for inline eval, shell-wrapper, and eval/source detection, so approval explanations and command-risk checks do not miss payloads hidden behind `exec`. Thanks @vincentkoc. - Google Meet: log the resolved audio provider model when starting Chrome and paired-node Meet talk-back bridges, so agent-mode joins show the STT model and bidi joins show the realtime voice model. diff --git a/src/infra/command-analysis/risks.test.ts b/src/infra/command-analysis/risks.test.ts index b9e4515e2eb..25d4b6899b4 100644 --- a/src/infra/command-analysis/risks.test.ts +++ b/src/infra/command-analysis/risks.test.ts @@ -91,8 +91,12 @@ describe("command-analysis risks", () => { it("detects env split-string flag forms", () => { expect(detectEnvSplitStringFlag(["env", "-S", "sh -c id"])).toBe("-S"); expect(detectEnvSplitStringFlag(["env", "-Ssh -c id"])).toBe("-S"); + expect(detectEnvSplitStringFlag(["env", "-iS", "sh -c id"])).toBe("-S"); + expect(detectEnvSplitStringFlag(["env", "-iSsh -c id"])).toBe("-S"); + expect(detectEnvSplitStringFlag(["env", "-is", "sh -c id"])).toBe("-s"); expect(detectEnvSplitStringFlag(["env", "--split-string=sh -c id"])).toBe("--split-string"); expect(detectEnvSplitStringFlag(["env", "sh", "-c", "id"])).toBeNull(); + expect(detectEnvSplitStringFlag(["env", "-XSsh -c id"])).toBeNull(); }); it("detects shell wrappers carried through prefix commands", () => { diff --git a/src/infra/command-analysis/risks.ts b/src/infra/command-analysis/risks.ts index 3bea90a9992..2bcd8aa5ac6 100644 --- a/src/infra/command-analysis/risks.ts +++ b/src/infra/command-analysis/risks.ts @@ -2,6 +2,7 @@ import { splitShellArgs } from "../../utils/shell-argv.js"; import { COMMAND_CARRIER_EXECUTABLES, isEnvAssignmentToken, + parseEnvInvocationPrelude, resolveCarrierCommandArgv, SOURCE_EXECUTABLES, } from "../command-carriers.js"; @@ -169,14 +170,31 @@ export function detectEnvSplitStringFlag(argv: string[]): string | null { if (normalizeExecutableToken(argv[0] ?? "") !== "env") { return null; } - for (const arg of argv.slice(1)) { + const parsed = parseEnvInvocationPrelude(argv); + if (!parsed?.splitArgv) { + return null; + } + for (const arg of argv.slice(1, parsed.commandIndex)) { const token = arg.trim(); - if (token === "-S" || token === "--split-string") { + if (token === "-S" || token === "-s") { return token; } + if (token === "--split-string") { + return "--split-string"; + } if (token.startsWith("--split-string=") || (token.startsWith("-S") && token.length > 2)) { return token.startsWith("--") ? "--split-string" : "-S"; } + if (token.startsWith("-") && !token.startsWith("--")) { + for (const option of token.slice(1)) { + if (option === "S") { + return "-S"; + } + if (option === "s") { + return "-s"; + } + } + } } return null; } diff --git a/src/infra/command-explainer/extract.test.ts b/src/infra/command-explainer/extract.test.ts index 2d3ba2b6442..7169ecc842b 100644 --- a/src/infra/command-explainer/extract.test.ts +++ b/src/infra/command-explainer/extract.test.ts @@ -602,6 +602,10 @@ describe("command explainer tree-sitter runtime", () => { expect(envSplitString.risks).toContainEqual( expect.objectContaining({ kind: "command-carrier", command: "env", flag: "-S" }), ); + const envCombinedSplitString = await explainShellCommand("env -iS 'sh -c \"id\"'"); + expect(envCombinedSplitString.risks).toContainEqual( + expect.objectContaining({ kind: "command-carrier", command: "env", flag: "-S" }), + ); for (const command of [ 'env python -c "print(1)"',