fix(exec): detect combined env split carriers

This commit is contained in:
Vincent Koc
2026-05-03 22:16:56 -07:00
parent 18db16471b
commit e2f4aa4617
4 changed files with 29 additions and 2 deletions

View File

@@ -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.

View File

@@ -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", () => {

View File

@@ -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;
}

View File

@@ -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)"',