infra: unwrap script wrapper approval targets (#55685)

* infra: unwrap script wrapper approvals

* infra: handle script short option values

* infra: gate script wrapper unwrapping by platform

* infra: narrow script wrapper option parsing
This commit is contained in:
Jacob Tomlinson
2026-03-27 03:05:35 -07:00
committed by GitHub
parent cb5f7e201f
commit 83da3cfe31
4 changed files with 96 additions and 0 deletions

View File

@@ -42,6 +42,8 @@ const TIME_FLAG_OPTIONS = new Set([
"--version",
]);
const TIME_OPTIONS_WITH_VALUE = new Set(["-f", "--format", "-o", "--output"]);
const BSD_SCRIPT_FLAG_OPTIONS = new Set(["-a", "-d", "-k", "-p", "-q", "-r"]);
const BSD_SCRIPT_OPTIONS_WITH_VALUE = new Set(["-F", "-t"]);
const TIMEOUT_FLAG_OPTIONS = new Set(["--foreground", "--preserve-status", "-v", "--verbose"]);
const TIMEOUT_OPTIONS_WITH_VALUE = new Set(["-k", "--kill-after", "-s", "--signal"]);
@@ -259,6 +261,47 @@ function unwrapTimeInvocation(argv: string[]): string[] | null {
});
}
function supportsScriptPositionalCommand(platform: NodeJS.Platform = process.platform): boolean {
return platform === "darwin" || platform === "freebsd";
}
function unwrapScriptInvocation(argv: string[]): string[] | null {
if (!supportsScriptPositionalCommand()) {
return null;
}
return scanWrapperInvocation(argv, {
separators: new Set(["--"]),
onToken: (token, lower) => {
if (!lower.startsWith("-") || lower === "-") {
return "stop";
}
const [flag] = token.split("=", 2);
if (BSD_SCRIPT_OPTIONS_WITH_VALUE.has(flag)) {
return token.includes("=") ? "continue" : "consume-next";
}
if (BSD_SCRIPT_FLAG_OPTIONS.has(flag)) {
return "continue";
}
return "invalid";
},
adjustCommandIndex: (commandIndex, currentArgv) => {
let sawTranscript = false;
for (let idx = commandIndex; idx < currentArgv.length; idx += 1) {
const token = currentArgv[idx]?.trim() ?? "";
if (!token) {
continue;
}
if (!sawTranscript) {
sawTranscript = true;
continue;
}
return idx;
}
return null;
},
});
}
function unwrapTimeoutInvocation(argv: string[]): string[] | null {
return unwrapDashOptionInvocation(argv, {
onFlag: (flag, lower) => {
@@ -294,6 +337,7 @@ const DISPATCH_WRAPPER_SPECS: readonly DispatchWrapperSpec[] = [
{ name: "ionice" },
{ name: "nice", unwrap: unwrapNiceInvocation, transparentUsage: true },
{ name: "nohup", unwrap: unwrapNohupInvocation, transparentUsage: true },
{ name: "script", unwrap: unwrapScriptInvocation, transparentUsage: true },
{ name: "setsid" },
{ name: "stdbuf", unwrap: unwrapStdbufInvocation, transparentUsage: true },
{ name: "sudo" },

View File

@@ -619,6 +619,23 @@ $0 \\"$1\\"" touch {marker}`,
});
});
it("prevents allow-always bypass for script wrapper chains", () => {
if (process.platform !== "darwin" && process.platform !== "freebsd") {
return;
}
const dir = makeTempDir();
const echo = makeExecutable(dir, "echo");
makeExecutable(dir, "id");
const env = makePathEnv(dir);
expectAllowAlwaysBypassBlocked({
dir,
firstCommand: "/usr/bin/script -q /dev/null /bin/sh -lc 'echo warmup-ok'",
secondCommand: "/usr/bin/script -q /dev/null /bin/sh -lc 'id > marker'",
env,
persistedPattern: echo,
});
});
it("does not persist comment-tailed payload paths that never execute", () => {
if (process.platform === "win32") {
return;

View File

@@ -13,6 +13,10 @@ import {
unwrapKnownShellMultiplexerInvocation,
} from "./exec-wrapper-resolution.js";
function supportsScriptPositionalCommandForTests(): boolean {
return process.platform === "darwin" || process.platform === "freebsd";
}
describe("basenameLower", () => {
test.each([
{ token: " Bun.CMD ", expected: "bun.cmd" },
@@ -40,6 +44,7 @@ describe("normalizeExecutableToken", () => {
describe("wrapper classification", () => {
test.each([
{ token: "sudo", dispatch: true, shell: false },
{ token: "script", dispatch: true, shell: false },
{ token: "time", dispatch: true, shell: false },
{ token: "timeout.exe", dispatch: true, shell: false },
{ token: "bash", dispatch: false, shell: true },
@@ -119,6 +124,16 @@ describe("unwrapKnownDispatchWrapperInvocation", () => {
argv: ["nohup", "--", "bash", "-lc", "echo hi"],
expected: { kind: "unwrapped", wrapper: "nohup", argv: ["bash", "-lc", "echo hi"] },
},
{
argv: ["script", "-q", "/dev/null", "bash", "-lc", "echo hi"],
expected: supportsScriptPositionalCommandForTests()
? { kind: "unwrapped", wrapper: "script", argv: ["bash", "-lc", "echo hi"] }
: { kind: "blocked", wrapper: "script" },
},
{
argv: ["script", "-E", "always", "/dev/null", "bash", "-lc", "echo hi"],
expected: { kind: "blocked", wrapper: "script" },
},
{
argv: ["stdbuf", "-o", "L", "bash", "-lc", "echo hi"],
expected: { kind: "unwrapped", wrapper: "stdbuf", argv: ["bash", "-lc", "echo hi"] },
@@ -131,6 +146,10 @@ describe("unwrapKnownDispatchWrapperInvocation", () => {
argv: ["timeout", "--signal=TERM", "5s", "bash", "-lc", "echo hi"],
expected: { kind: "unwrapped", wrapper: "timeout", argv: ["bash", "-lc", "echo hi"] },
},
{
argv: ["script", "-q", "/dev/null"],
expected: { kind: "blocked", wrapper: "script" },
},
{
argv: ["sudo", "bash", "-lc", "echo hi"],
expected: { kind: "blocked", wrapper: "sudo" },

View File

@@ -18,6 +18,22 @@ describe("resolveExecWrapperTrustPlan", () => {
});
});
test("unwraps script wrappers before evaluating nested shell payloads", () => {
if (process.platform !== "darwin" && process.platform !== "freebsd") {
return;
}
expect(
resolveExecWrapperTrustPlan(["/usr/bin/script", "-q", "/dev/null", "sh", "-lc", "echo hi"]),
).toEqual({
argv: ["sh", "-lc", "echo hi"],
policyArgv: ["sh", "-lc", "echo hi"],
wrapperChain: ["script"],
policyBlocked: false,
shellWrapperExecutable: true,
shellInlineCommand: "echo hi",
});
});
test("fails closed for unsupported shell multiplexer applets", () => {
expect(resolveExecWrapperTrustPlan(["busybox", "sed", "-n", "1p"])).toEqual({
argv: ["busybox", "sed", "-n", "1p"],