Skip to content

Commit e2f4aa4

Browse files
committed
fix(exec): detect combined env split carriers
1 parent 18db164 commit e2f4aa4

4 files changed

Lines changed: 29 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ Docs: https://docs.openclaw.ai
5050

5151
### Fixes
5252

53+
- 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.
5354
- 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.
5455
- 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.
5556
- 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.

src/infra/command-analysis/risks.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,12 @@ describe("command-analysis risks", () => {
9191
it("detects env split-string flag forms", () => {
9292
expect(detectEnvSplitStringFlag(["env", "-S", "sh -c id"])).toBe("-S");
9393
expect(detectEnvSplitStringFlag(["env", "-Ssh -c id"])).toBe("-S");
94+
expect(detectEnvSplitStringFlag(["env", "-iS", "sh -c id"])).toBe("-S");
95+
expect(detectEnvSplitStringFlag(["env", "-iSsh -c id"])).toBe("-S");
96+
expect(detectEnvSplitStringFlag(["env", "-is", "sh -c id"])).toBe("-s");
9497
expect(detectEnvSplitStringFlag(["env", "--split-string=sh -c id"])).toBe("--split-string");
9598
expect(detectEnvSplitStringFlag(["env", "sh", "-c", "id"])).toBeNull();
99+
expect(detectEnvSplitStringFlag(["env", "-XSsh -c id"])).toBeNull();
96100
});
97101

98102
it("detects shell wrappers carried through prefix commands", () => {

src/infra/command-analysis/risks.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { splitShellArgs } from "../../utils/shell-argv.js";
22
import {
33
COMMAND_CARRIER_EXECUTABLES,
44
isEnvAssignmentToken,
5+
parseEnvInvocationPrelude,
56
resolveCarrierCommandArgv,
67
SOURCE_EXECUTABLES,
78
} from "../command-carriers.js";
@@ -169,14 +170,31 @@ export function detectEnvSplitStringFlag(argv: string[]): string | null {
169170
if (normalizeExecutableToken(argv[0] ?? "") !== "env") {
170171
return null;
171172
}
172-
for (const arg of argv.slice(1)) {
173+
const parsed = parseEnvInvocationPrelude(argv);
174+
if (!parsed?.splitArgv) {
175+
return null;
176+
}
177+
for (const arg of argv.slice(1, parsed.commandIndex)) {
173178
const token = arg.trim();
174-
if (token === "-S" || token === "--split-string") {
179+
if (token === "-S" || token === "-s") {
175180
return token;
176181
}
182+
if (token === "--split-string") {
183+
return "--split-string";
184+
}
177185
if (token.startsWith("--split-string=") || (token.startsWith("-S") && token.length > 2)) {
178186
return token.startsWith("--") ? "--split-string" : "-S";
179187
}
188+
if (token.startsWith("-") && !token.startsWith("--")) {
189+
for (const option of token.slice(1)) {
190+
if (option === "S") {
191+
return "-S";
192+
}
193+
if (option === "s") {
194+
return "-s";
195+
}
196+
}
197+
}
180198
}
181199
return null;
182200
}

src/infra/command-explainer/extract.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -602,6 +602,10 @@ describe("command explainer tree-sitter runtime", () => {
602602
expect(envSplitString.risks).toContainEqual(
603603
expect.objectContaining({ kind: "command-carrier", command: "env", flag: "-S" }),
604604
);
605+
const envCombinedSplitString = await explainShellCommand("env -iS 'sh -c \"id\"'");
606+
expect(envCombinedSplitString.risks).toContainEqual(
607+
expect.objectContaining({ kind: "command-carrier", command: "env", flag: "-S" }),
608+
);
605609

606610
for (const command of [
607611
'env python -c "print(1)"',

0 commit comments

Comments
 (0)