Skip to content

Commit faa66b6

Browse files
committed
fix: recheck rebuilt system run argv
1 parent 5e0850f commit faa66b6

3 files changed

Lines changed: 149 additions & 1 deletion

File tree

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { describe, expect, it } from "vitest";
2+
import { analyzeShellCommand } from "../infra/exec-approvals-analysis.js";
3+
import { resolveExecApprovalsFromFile } from "../infra/exec-approvals.js";
4+
import { resolveExecSafeBinRuntimePolicy } from "../infra/exec-safe-bin-runtime-policy.js";
5+
import { resolveSystemRunExecArgv } from "./invoke-system-run-allowlist.js";
6+
7+
function resolveAllowlistApprovals() {
8+
return resolveExecApprovalsFromFile({
9+
file: {
10+
version: 1,
11+
defaults: {
12+
security: "allowlist",
13+
ask: "off",
14+
askFallback: "deny",
15+
},
16+
},
17+
});
18+
}
19+
20+
describe("resolveSystemRunExecArgv", () => {
21+
it.runIf(process.platform !== "win32")(
22+
"keeps rebuilt shell argv behind a final allowlist check",
23+
() => {
24+
const env = { PATH: "/usr/bin:/bin" };
25+
const analysis = analyzeShellCommand({
26+
command: "head -c 16",
27+
env,
28+
platform: process.platform,
29+
});
30+
expect(analysis.ok).toBe(true);
31+
if (!analysis.ok) {
32+
return;
33+
}
34+
35+
const result = resolveSystemRunExecArgv({
36+
plannedAllowlistArgv: undefined,
37+
argv: ["/bin/sh", "-lc", "head -c 16"],
38+
security: "allowlist",
39+
approvals: resolveAllowlistApprovals(),
40+
safeBins: new Set(),
41+
safeBinProfiles: {},
42+
trustedSafeBinDirs: new Set(),
43+
skillBins: [],
44+
autoAllowSkills: false,
45+
isWindows: false,
46+
policy: {
47+
approvedByAsk: false,
48+
analysisOk: true,
49+
allowlistSatisfied: true,
50+
},
51+
shellCommand: "head -c 16",
52+
segments: analysis.segments,
53+
segmentSatisfiedBy: ["safeBins"],
54+
cwd: undefined,
55+
env,
56+
});
57+
58+
expect(result).toBeNull();
59+
},
60+
);
61+
62+
it.runIf(process.platform !== "win32")(
63+
"returns rebuilt shell argv when the final allowlist check passes",
64+
() => {
65+
const env = { PATH: "/usr/bin:/bin" };
66+
const analysis = analyzeShellCommand({
67+
command: "head -c 16",
68+
env,
69+
platform: process.platform,
70+
});
71+
expect(analysis.ok).toBe(true);
72+
if (!analysis.ok) {
73+
return;
74+
}
75+
const safeBinPolicy = resolveExecSafeBinRuntimePolicy({
76+
global: { safeBins: ["head"] },
77+
});
78+
79+
const result = resolveSystemRunExecArgv({
80+
plannedAllowlistArgv: undefined,
81+
argv: ["/bin/sh", "-lc", "head -c 16"],
82+
security: "allowlist",
83+
approvals: resolveAllowlistApprovals(),
84+
safeBins: safeBinPolicy.safeBins,
85+
safeBinProfiles: safeBinPolicy.safeBinProfiles,
86+
trustedSafeBinDirs: safeBinPolicy.trustedSafeBinDirs,
87+
skillBins: [],
88+
autoAllowSkills: false,
89+
isWindows: false,
90+
policy: {
91+
approvedByAsk: false,
92+
analysisOk: true,
93+
allowlistSatisfied: true,
94+
},
95+
shellCommand: "head -c 16",
96+
segments: analysis.segments,
97+
segmentSatisfiedBy: ["safeBins"],
98+
cwd: undefined,
99+
env,
100+
});
101+
102+
expect(result).not.toBeNull();
103+
expect(result?.[0]).toBe("/bin/sh");
104+
expect(result?.[2]).toContain("head");
105+
expect(result?.[2]).not.toBe("head -c 16");
106+
},
107+
);
108+
});

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,12 @@ export function resolveSystemRunExecArgv(params: {
123123
plannedAllowlistArgv: string[] | undefined;
124124
argv: string[];
125125
security: ExecSecurity;
126+
approvals: ReturnType<typeof resolveExecApprovals>;
127+
safeBins: ReturnType<typeof resolveExecSafeBinRuntimePolicy>["safeBins"];
128+
safeBinProfiles: ReturnType<typeof resolveExecSafeBinRuntimePolicy>["safeBinProfiles"];
129+
trustedSafeBinDirs: ReturnType<typeof resolveExecSafeBinRuntimePolicy>["trustedSafeBinDirs"];
130+
skillBins: SkillBinTrustEntry[];
131+
autoAllowSkills: boolean;
126132
isWindows: boolean;
127133
policy: {
128134
approvedByAsk: boolean;
@@ -177,6 +183,22 @@ export function resolveSystemRunExecArgv(params: {
177183
if (!rewrittenArgv) {
178184
return null;
179185
}
186+
const rebuiltAllowlist = evaluateSystemRunAllowlist({
187+
shellCommand: rebuilt.command,
188+
argv: rewrittenArgv,
189+
approvals: params.approvals,
190+
security: params.security,
191+
safeBins: params.safeBins,
192+
safeBinProfiles: params.safeBinProfiles,
193+
trustedSafeBinDirs: params.trustedSafeBinDirs,
194+
cwd: params.cwd,
195+
env: params.env,
196+
skillBins: params.skillBins,
197+
autoAllowSkills: params.autoAllowSkills,
198+
});
199+
if (!rebuiltAllowlist.analysisOk || !rebuiltAllowlist.allowlistSatisfied) {
200+
return null;
201+
}
180202
execArgv = rewrittenArgv;
181203
}
182204
return execArgv;

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

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ import {
1616
type ExecAllowlistEntry,
1717
type ExecAsk,
1818
type ExecCommandSegment,
19+
type ExecSegmentSatisfiedBy,
1920
type ExecSecurity,
21+
type SkillBinTrustEntry,
2022
} from "../infra/exec-approvals.js";
2123
import type { ExecHostRequest, ExecHostResponse, ExecHostRunResult } from "../infra/exec-host.js";
2224
import { resolveExecSafeBinRuntimePolicy } from "../infra/exec-safe-bin-runtime-policy.js";
@@ -111,8 +113,13 @@ type SystemRunPolicyPhase = SystemRunParsePhase & {
111113
allowlistMatches: ExecAllowlistEntry[];
112114
analysisOk: boolean;
113115
allowlistSatisfied: boolean;
116+
safeBins: ReturnType<typeof resolveExecSafeBinRuntimePolicy>["safeBins"];
117+
safeBinProfiles: ReturnType<typeof resolveExecSafeBinRuntimePolicy>["safeBinProfiles"];
118+
trustedSafeBinDirs: ReturnType<typeof resolveExecSafeBinRuntimePolicy>["trustedSafeBinDirs"];
119+
skillBins: SkillBinTrustEntry[];
120+
autoAllowSkills: boolean;
114121
segments: ExecCommandSegment[];
115-
segmentSatisfiedBy: import("../infra/exec-approvals.js").ExecSegmentSatisfiedBy[];
122+
segmentSatisfiedBy: ExecSegmentSatisfiedBy[];
116123
plannedAllowlistArgv: string[] | undefined;
117124
isWindows: boolean;
118125
approvedCwdSnapshot: ApprovedCwdSnapshot | undefined;
@@ -523,6 +530,11 @@ async function evaluateSystemRunPolicyPhase(
523530
allowlistMatches,
524531
analysisOk,
525532
allowlistSatisfied,
533+
safeBins,
534+
safeBinProfiles,
535+
trustedSafeBinDirs,
536+
skillBins: bins,
537+
autoAllowSkills,
526538
segments,
527539
segmentSatisfiedBy,
528540
plannedAllowlistArgv: plannedAllowlistArgv ?? undefined,
@@ -589,6 +601,12 @@ async function executeSystemRunPhase(
589601
plannedAllowlistArgv: phase.plannedAllowlistArgv,
590602
argv: phase.argv,
591603
security: phase.security,
604+
approvals: phase.approvals,
605+
safeBins: phase.safeBins,
606+
safeBinProfiles: phase.safeBinProfiles,
607+
trustedSafeBinDirs: phase.trustedSafeBinDirs,
608+
skillBins: phase.skillBins,
609+
autoAllowSkills: phase.autoAllowSkills,
592610
isWindows: phase.isWindows,
593611
policy: phase.policy,
594612
shellCommand: phase.shellPayload,

0 commit comments

Comments
 (0)