fix(exec): match bare * wildcard in allowlist entries (#25082)

The matchAllowlist() function skipped patterns without path separators
(/, \, ~), causing a bare "*" wildcard entry to never reach the glob
matcher. Since glob's single * maps to [^/]*, it would also fail against
absolute paths. Handle bare "*" as a special case that matches any
resolved executable path.

Closes #25082
This commit is contained in:
Marcus Widing
2026-02-24 10:26:03 +01:00
committed by Peter Steinberger
parent e9216cb7dc
commit 0f0b2c0255
2 changed files with 53 additions and 1 deletions

View File

@@ -43,6 +43,22 @@ describe("exec approvals allowlist matching", () => {
}
});
it("matches bare * wildcard pattern against any resolved path", () => {
const match = matchAllowlist([{ pattern: "*" }], baseResolution);
expect(match).not.toBeNull();
expect(match?.pattern).toBe("*");
});
it("matches bare * wildcard against arbitrary executables", () => {
const match = matchAllowlist([{ pattern: "*" }], {
rawExecutable: "python3",
resolvedPath: "/usr/bin/python3",
executableName: "python3",
});
expect(match).not.toBeNull();
expect(match?.pattern).toBe("*");
});
it("requires a resolved path", () => {
const match = matchAllowlist([{ pattern: "bin/rg" }], {
rawExecutable: "bin/rg",
@@ -543,6 +559,26 @@ describe("exec approvals shell allowlist (chained commands)", () => {
expect(result.analysisOk).toBe(false);
expect(result.allowlistSatisfied).toBe(false);
});
it("satisfies allowlist when bare * wildcard is present", () => {
const dir = makeTempDir();
const binPath = path.join(dir, "mybin");
fs.writeFileSync(binPath, "#!/bin/sh\n", { mode: 0o755 });
const env = makePathEnv(dir);
try {
const result = evaluateShellAllowlist({
command: "mybin --flag",
allowlist: [{ pattern: "*" }],
safeBins: new Set(),
cwd: dir,
env,
});
expect(result.analysisOk).toBe(true);
expect(result.allowlistSatisfied).toBe(true);
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});
});
describe("exec approvals allowlist evaluation", () => {

View File

@@ -223,7 +223,17 @@ export function matchAllowlist(
entries: ExecAllowlistEntry[],
resolution: CommandResolution | null,
): ExecAllowlistEntry | null {
if (!entries.length || !resolution?.resolvedPath) {
if (!entries.length) {
return null;
}
// A bare "*" wildcard allows any command regardless of resolution.
// Check it before the resolvedPath guard so that unresolvable commands
// (e.g. Windows executables without known extensions) still match.
const bareWild = entries.find((e) => e.pattern?.trim() === "*");
if (bareWild && resolution) {
return bareWild;
}
if (!resolution?.resolvedPath) {
return null;
}
const resolvedPath = resolution.resolvedPath;
@@ -232,6 +242,12 @@ export function matchAllowlist(
if (!pattern) {
continue;
}
// A bare "*" wildcard means "allow any executable". Match immediately
// without going through glob expansion (glob `*` maps to `[^/]*` which
// would fail on absolute paths containing slashes).
if (pattern === "*") {
return entry;
}
const hasPath = pattern.includes("/") || pattern.includes("\\") || pattern.includes("~");
if (!hasPath) {
continue;