From 0f0b2c0255e7d683ab79d5e5b8526c142ceb3bd7 Mon Sep 17 00:00:00 2001 From: Marcus Widing Date: Tue, 24 Feb 2026 10:26:03 +0100 Subject: [PATCH] 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 --- src/infra/exec-approvals.test.ts | 36 ++++++++++++++++++++++++++++ src/infra/exec-command-resolution.ts | 18 +++++++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/src/infra/exec-approvals.test.ts b/src/infra/exec-approvals.test.ts index 6b405b466..407261f43 100644 --- a/src/infra/exec-approvals.test.ts +++ b/src/infra/exec-approvals.test.ts @@ -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", () => { diff --git a/src/infra/exec-command-resolution.ts b/src/infra/exec-command-resolution.ts index 3dceb0fc5..c2c2f3fe2 100644 --- a/src/infra/exec-command-resolution.ts +++ b/src/infra/exec-command-resolution.ts @@ -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;