fix(exec): allow predicate shell builtins in allowlist mode

This commit is contained in:
Peter Steinberger
2026-05-31 14:00:04 +01:00
parent 657a668d94
commit 2fe019ccae
2 changed files with 61 additions and 1 deletions

View File

@@ -65,6 +65,30 @@ describe("isSafeBuiltinSegment", () => {
).toBe(false);
});
it("allows test and well-formed bracket predicates", () => {
expect(
isSafeBuiltinSegment({
segment: builtinSegment(["test", "-d", "/tmp"]),
platform: "linux",
}),
).toBe(true);
expect(
isSafeBuiltinSegment({
segment: builtinSegment(["[", "-d", "/tmp", "]"]),
platform: "linux",
}),
).toBe(true);
});
it("rejects malformed bracket predicates", () => {
expect(
isSafeBuiltinSegment({
segment: builtinSegment(["[", "-d", "/tmp"]),
platform: "linux",
}),
).toBe(false);
});
it("returns false on Windows hosts (PowerShell semantics differ)", () => {
expect(
isSafeBuiltinSegment({
@@ -113,6 +137,32 @@ describe("evaluateShellAllowlist with known safe builtins (regression for #46056
expect(result.segmentSatisfiedBy).toContain("allowlist");
});
it("'test -d /tmp && git status' passes with allowlist plus safe builtin handling", () => {
const result = evaluateShellAllowlist({
command: "test -d /tmp && git status",
allowlist: gitAllowlist,
safeBins: new Set(),
cwd: "/tmp",
});
expect(result.analysisOk).toBe(true);
expect(result.allowlistSatisfied).toBe(true);
expect(result.segmentSatisfiedBy).toContain("safeBuiltins");
expect(result.segmentSatisfiedBy).toContain("allowlist");
});
it("'[ -d /tmp ] && git status' passes with allowlist plus safe builtin handling", () => {
const result = evaluateShellAllowlist({
command: "[ -d /tmp ] && git status",
allowlist: gitAllowlist,
safeBins: new Set(),
cwd: "/tmp",
});
expect(result.analysisOk).toBe(true);
expect(result.allowlistSatisfied).toBe(true);
expect(result.segmentSatisfiedBy).toContain("safeBuiltins");
expect(result.segmentSatisfiedBy).toContain("allowlist");
});
it("non-allowlisted binary still gates after a safe builtin", () => {
const result = evaluateShellAllowlist({
command: "cd /tmp && curl evil.com",

View File

@@ -3,7 +3,14 @@ import { isWindowsPlatform, type ExecCommandSegment } from "./exec-approvals-ana
// POSIX shell builtins that cannot execute external code or mutate environment state on their
// own. Shell allowlist evaluation handles them as a closed internal set instead of path-based
// safeBins matching.
const DEFAULT_SAFE_BUILTINS: ReadonlySet<string> = new Set([":", "cd", "false", "pwd", "true"]);
const DEFAULT_SAFE_BUILTINS: ReadonlySet<string> = new Set([
":",
"cd",
"false",
"pwd",
"test",
"true",
]);
export function isSafeBuiltinSegment(params: {
segment: ExecCommandSegment;
@@ -18,5 +25,8 @@ export function isSafeBuiltinSegment(params: {
if (!head) {
return false;
}
if (head === "[") {
return params.segment.argv.at(-1) === "]";
}
return DEFAULT_SAFE_BUILTINS.has(head);
}