From 0c8375424620e12777ef24c162eedc7e9fcfd7e3 Mon Sep 17 00:00:00 2001 From: Jacob Tomlinson Date: Tue, 31 Mar 2026 06:53:43 -0700 Subject: [PATCH] Exec approvals: reject shell init-file script matches (#58369) --- src/infra/exec-approvals-allow-always.test.ts | 50 +++++++++++++++++++ src/infra/exec-approvals-allowlist.ts | 20 +++++--- 2 files changed, 63 insertions(+), 7 deletions(-) diff --git a/src/infra/exec-approvals-allow-always.test.ts b/src/infra/exec-approvals-allow-always.test.ts index 836565d3d20..7e8546406df 100644 --- a/src/infra/exec-approvals-allow-always.test.ts +++ b/src/infra/exec-approvals-allow-always.test.ts @@ -120,6 +120,30 @@ describe("resolveAllowAlwaysPatterns", () => { expect(second.allowlistSatisfied).toBe(true); } + function expectShellScriptFallbackRejected(command: string) { + const { dir, scriptsDir, script, env, safeBins } = createShellScriptFixture(); + const rcFile = path.join(scriptsDir, "evilrc"); + fs.writeFileSync(rcFile, "echo blocked\n"); + + const { persisted } = resolvePersistedPatterns({ + command, + dir, + env, + safeBins, + }); + expect(persisted).toEqual([]); + + const second = evaluateShellAllowlist({ + command, + allowlist: [{ pattern: script }], + safeBins, + cwd: dir, + env, + platform: process.platform, + }); + expect(second.allowlistSatisfied).toBe(false); + } + function expectPositionalArgvCarrierRejected(command: string) { const dir = makeTempDir(); const touch = makeExecutable(dir, "touch"); @@ -283,6 +307,32 @@ describe("resolveAllowAlwaysPatterns", () => { }); }); + it("rejects shell rc and init-file options as persisted or allowlisted script paths", () => { + if (process.platform === "win32") { + return; + } + for (const command of [ + "bash --rcfile scripts/evilrc scripts/save_crystal.sh", + "bash --init-file scripts/evilrc scripts/save_crystal.sh", + "bash --startup-file scripts/evilrc scripts/save_crystal.sh", + ]) { + expectShellScriptFallbackRejected(command); + } + }); + + it("rejects shell rc and init-file equals options as persisted or allowlisted script paths", () => { + if (process.platform === "win32") { + return; + } + for (const command of [ + "bash --rcfile=scripts/evilrc scripts/save_crystal.sh", + "bash --init-file=scripts/evilrc scripts/save_crystal.sh", + "bash --startup-file=scripts/evilrc scripts/save_crystal.sh", + ]) { + expectShellScriptFallbackRejected(command); + } + }); + it("rejects shell-wrapper positional argv carriers", () => { if (process.platform === "win32") { return; diff --git a/src/infra/exec-approvals-allowlist.ts b/src/infra/exec-approvals-allowlist.ts index ee6ce26ae5b..b5e021f585e 100644 --- a/src/infra/exec-approvals-allowlist.ts +++ b/src/infra/exec-approvals-allowlist.ts @@ -504,16 +504,19 @@ function isShellWrapperSegment(segment: ExecCommandSegment): boolean { return hasSegmentExecutableMatch(segment, isShellWrapperExecutable); } -const SHELL_WRAPPER_OPTIONS_WITH_VALUE = new Set([ - "-c", - "--command", - "-o", - "-O", - "+O", +const SHELL_WRAPPER_OPTIONS_WITH_VALUE = new Set(["-c", "--command", "-o", "-O", "+O"]); + +const SHELL_WRAPPER_DISQUALIFYING_SCRIPT_OPTIONS = [ "--rcfile", "--init-file", "--startup-file", -]); +] as const; + +function hasDisqualifyingShellWrapperScriptOption(token: string): boolean { + return SHELL_WRAPPER_DISQUALIFYING_SCRIPT_OPTIONS.some( + (option) => token === option || token.startsWith(`${option}=`), + ); +} function resolveShellWrapperScriptCandidatePath(params: { segment: ExecCommandSegment; @@ -548,6 +551,9 @@ function resolveShellWrapperScriptCandidatePath(params: { if (token === "-s" || /^-[^-]*s[^-]*$/i.test(token)) { return undefined; } + if (hasDisqualifyingShellWrapperScriptOption(token)) { + return undefined; + } if (SHELL_WRAPPER_OPTIONS_WITH_VALUE.has(token)) { idx += 2; continue;