Skip to content

Commit 3168987

Browse files
authored
fix(exec): gate versioned inline interpreters (#96216)
* fix(exec): gate versioned inline interpreters * fix(exec): trim unsupported R inline flag * fix(exec): avoid r2 inline eval collision
1 parent 7e2b2d2 commit 3168987

3 files changed

Lines changed: 61 additions & 7 deletions

File tree

src/infra/command-analysis/inline-eval.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,14 @@ function expectInlineEvalDescription(hit: InterpreterInlineEvalHit | null, expec
1818
describe("exec inline eval detection", () => {
1919
it.each([
2020
{ argv: ["python3", "-c", "print('hi')"], expected: "python3 -c" },
21+
{ argv: ["python3.13", "-c", "print('hi')"], expected: "python3.13 -c" },
22+
{ argv: ["/usr/bin/pypy3.10", "-c", "print('hi')"], expected: "pypy3.10 -c" },
2123
{ argv: ["/usr/bin/node", "--eval", "console.log('hi')"], expected: "node --eval" },
2224
{ argv: ["perl", "-E", "say 1"], expected: "perl -e" },
25+
{ argv: ["php", "-B", "system('id');"], expected: "php -B" },
26+
{ argv: ["php", "-E", "system('id');"], expected: "php -E" },
27+
{ argv: ["php", "-R", "system('id');"], expected: "php -R" },
28+
{ argv: ["Rscript", "-e", "system('id')"], expected: "rscript -e" },
2329
{ argv: ["osascript", "-e", "beep"], expected: "osascript -e" },
2430
{ argv: ["awk", "BEGIN { print 1 }"], expected: "awk inline program" },
2531
{ argv: ["gawk", "-F", ",", "{print $1}", "data.csv"], expected: "gawk inline program" },
@@ -60,7 +66,11 @@ describe("exec inline eval detection", () => {
6066

6167
it("ignores normal script execution", () => {
6268
expect(detectInterpreterInlineEvalArgv(["python3", "script.py"])).toBeNull();
69+
expect(detectInterpreterInlineEvalArgv(["python3.13", "script.py"])).toBeNull();
6370
expect(detectInterpreterInlineEvalArgv(["node", "script.js"])).toBeNull();
71+
expect(detectInterpreterInlineEvalArgv(["php", "-F", "filter.php"])).toBeNull();
72+
expect(detectInterpreterInlineEvalArgv(["Rscript", "script.R"])).toBeNull();
73+
expect(detectInterpreterInlineEvalArgv(["r2", "-e", "bin.cache=true", "program"])).toBeNull();
6474
expect(detectInterpreterInlineEvalArgv(["awk", "-f", "script.awk", "data.csv"])).toBeNull();
6575
expect(detectInterpreterInlineEvalArgv(["find", ".", "-name", "*.ts"])).toBeNull();
6676
expect(detectInterpreterInlineEvalArgv(["xargs", "-0"])).toBeNull();
@@ -76,7 +86,12 @@ describe("exec inline eval detection", () => {
7686

7787
it("matches interpreter-like allowlist patterns", () => {
7888
expect(isInterpreterLikeAllowlistPattern("/usr/bin/python3")).toBe(true);
89+
expect(isInterpreterLikeAllowlistPattern("/usr/bin/python3.13")).toBe(true);
90+
expect(isInterpreterLikeAllowlistPattern("python3.*")).toBe(true);
91+
expect(isInterpreterLikeAllowlistPattern("pypy3.10")).toBe(true);
7992
expect(isInterpreterLikeAllowlistPattern("**/node")).toBe(true);
93+
expect(isInterpreterLikeAllowlistPattern("Rscript")).toBe(true);
94+
expect(isInterpreterLikeAllowlistPattern("r2")).toBe(false);
8095
expect(isInterpreterLikeAllowlistPattern("/usr/bin/awk")).toBe(true);
8196
expect(isInterpreterLikeAllowlistPattern("**/gawk")).toBe(true);
8297
expect(isInterpreterLikeAllowlistPattern("/usr/bin/mawk")).toBe(true);

src/infra/command-analysis/inline-eval.ts

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ type PositionalInterpreterSpec = {
3434
flag: "<command>" | "<program>";
3535
};
3636

37+
const VERSION_SUFFIX_PATTERN = /-?\d+(?:\.\d+)*$/;
38+
3739
const FLAG_INTERPRETER_INLINE_EVAL_SPECS: readonly InterpreterFlagSpec[] = [
3840
{ names: ["python", "python2", "python3", "pypy", "pypy3"], exactFlags: new Set(["-c"]) },
3941
{
@@ -47,7 +49,16 @@ const FLAG_INTERPRETER_INLINE_EVAL_SPECS: readonly InterpreterFlagSpec[] = [
4749
},
4850
{ names: ["ruby"], exactFlags: new Set(["-e"]) },
4951
{ names: ["perl"], exactFlags: new Set(["-e", "-E"]) },
50-
{ names: ["php"], exactFlags: new Set(["-r"]) },
52+
{
53+
names: ["php"],
54+
exactFlags: new Set(["-r"]),
55+
rawExactFlags: new Map([
56+
["-B", "-B"],
57+
["-E", "-E"],
58+
["-R", "-R"],
59+
]),
60+
},
61+
{ names: ["r", "rscript"], exactFlags: new Set(["-e"]) },
5162
{ names: ["lua"], exactFlags: new Set(["-e"]) },
5263
{ names: ["osascript"], exactFlags: new Set(["-e"]) },
5364
{
@@ -154,10 +165,28 @@ const INTERPRETER_ALLOWLIST_NAMES = new Set(
154165
),
155166
);
156167

168+
function stripInterpreterVersionSuffix(value: string): string {
169+
const stripped = value.replace(VERSION_SUFFIX_PATTERN, "");
170+
return stripped.length > 0 ? stripped : value;
171+
}
172+
173+
function interpreterNameVariants(value: string): readonly string[] {
174+
const stripped = stripInterpreterVersionSuffix(value);
175+
// Do not synthesize one-letter interpreter names: commands like r2 can use
176+
// their own eval flags and must not be mistaken for R inline execution.
177+
return stripped === value || stripped.length < 2 ? [value] : [value, stripped];
178+
}
179+
180+
function specNamesInclude(names: readonly string[], normalizedExecutable: string): boolean {
181+
return interpreterNameVariants(normalizedExecutable).some((candidate) =>
182+
names.includes(candidate),
183+
);
184+
}
185+
157186
function findInterpreterSpec(executable: string): InterpreterFlagSpec | null {
158187
const normalized = normalizeExecutableToken(executable);
159188
for (const spec of FLAG_INTERPRETER_INLINE_EVAL_SPECS) {
160-
if (spec.names.includes(normalized)) {
189+
if (specNamesInclude(spec.names, normalized)) {
161190
return spec;
162191
}
163192
}
@@ -167,7 +196,7 @@ function findInterpreterSpec(executable: string): InterpreterFlagSpec | null {
167196
function findPositionalInterpreterSpec(executable: string): PositionalInterpreterSpec | null {
168197
const normalized = normalizeExecutableToken(executable);
169198
for (const spec of POSITIONAL_INTERPRETER_INLINE_EVAL_SPECS) {
170-
if (spec.names.includes(normalized)) {
199+
if (specNamesInclude(spec.names, normalized)) {
171200
return spec;
172201
}
173202
}
@@ -299,11 +328,17 @@ export function isInterpreterLikeAllowlistPattern(pattern: string | undefined |
299328
return false;
300329
}
301330
const normalized = normalizeExecutableToken(trimmed);
302-
if (INTERPRETER_ALLOWLIST_NAMES.has(normalized)) {
331+
if (
332+
interpreterNameVariants(normalized).some((candidate) =>
333+
INTERPRETER_ALLOWLIST_NAMES.has(candidate),
334+
)
335+
) {
303336
return true;
304337
}
305338
const basename = trimmed.replace(/\\/g, "/").split("/").pop() ?? trimmed;
306339
const withoutExe = basename.endsWith(".exe") ? basename.slice(0, -4) : basename;
307-
const strippedWildcards = withoutExe.replace(/[*?[\]{}()]/g, "");
308-
return INTERPRETER_ALLOWLIST_NAMES.has(strippedWildcards);
340+
const strippedWildcards = withoutExe.replace(/[*?[\]{}()]/g, "").replace(/[.-]+$/, "");
341+
return interpreterNameVariants(strippedWildcards).some((candidate) =>
342+
INTERPRETER_ALLOWLIST_NAMES.has(candidate),
343+
);
309344
}

src/node-host/invoke-system-run.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1601,6 +1601,10 @@ describe("handleSystemRunInvoke mac app exec host routing", () => {
16011601
command: ["python3", "-c", "print('hi')"],
16021602
expected: "python3 -c requires explicit approval in strictInlineEval mode",
16031603
},
1604+
{
1605+
command: ["python3.13", "-c", "print('hi')"],
1606+
expected: "python3.13 -c requires explicit approval in strictInlineEval mode",
1607+
},
16041608
] as const;
16051609
setRuntimeConfigSnapshot({
16061610
tools: {
@@ -1672,7 +1676,7 @@ describe("handleSystemRunInvoke mac app exec host routing", () => {
16721676
const tempDir = createFixtureDir("openclaw-inline-eval-bin-");
16731677
const executablePath = createTempExecutable({
16741678
dir: tempDir,
1675-
name: "python3",
1679+
name: "python3.13",
16761680
});
16771681
const { runCommand, sendInvokeResult } = await runSystemInvoke({
16781682
preferMacAppExecHost: false,

0 commit comments

Comments
 (0)