Exec approvals: reject wrapper carrier allow-always targets (#55947)

* Exec approvals: reject wrapper carrier allow-always targets

Co-authored-by: nexrin <268879349+nexrin@users.noreply.github.com>

* Tests: add shell wrapper carrier follow-up assertion

---------

Co-authored-by: nexrin <268879349+nexrin@users.noreply.github.com>
This commit is contained in:
Jacob Tomlinson
2026-03-27 12:07:47 -07:00
committed by GitHub
parent 7ce2670043
commit 9ec44fad39
2 changed files with 64 additions and 0 deletions

View File

@@ -652,4 +652,60 @@ $0 \\"$1\\"" touch {marker}`,
persistedPattern: benign,
});
});
it("rejects positional carrier when carried executable is a dispatch wrapper", () => {
if (process.platform === "win32") {
return;
}
const dir = makeTempDir();
makeExecutable(dir, "env");
const env = makePathEnv(dir);
const safeBins = resolveSafeBins(undefined);
const { persisted } = resolvePersistedPatterns({
command: `sh -lc '$0 "$@"' env echo SAFE`,
dir,
env,
safeBins,
});
expect(persisted).toEqual([]);
const second = evaluateShellAllowlist({
command: `sh -lc '$0 "$@"' env BASH_ENV=/tmp/payload.sh bash -lc 'id > /tmp/pwned'`,
allowlist: persisted.map((pattern) => ({ pattern })),
safeBins,
cwd: dir,
env,
platform: process.platform,
});
expect(second.allowlistSatisfied).toBe(false);
});
it("rejects positional carrier when carried executable is a shell wrapper", () => {
if (process.platform === "win32") {
return;
}
const dir = makeTempDir();
makeExecutable(dir, "bash");
const env = makePathEnv(dir);
const safeBins = resolveSafeBins(undefined);
const { persisted } = resolvePersistedPatterns({
command: `sh -lc '$0 "$@"' bash -lc 'echo safe'`,
dir,
env,
safeBins,
});
expect(persisted).toEqual([]);
const second = evaluateShellAllowlist({
command: `sh -lc '$0 "$@"' bash -lc 'id > /tmp/pwned'`,
allowlist: persisted.map((pattern) => ({ pattern })),
safeBins,
cwd: dir,
env,
platform: process.platform,
});
expect(second.allowlistSatisfied).toBe(false);
});
});

View File

@@ -1,4 +1,5 @@
import path from "node:path";
import { isDispatchWrapperExecutable } from "./dispatch-wrapper-resolution.js";
import {
analyzeShellCommand,
isWindowsPlatform,
@@ -463,6 +464,13 @@ function resolveShellWrapperPositionalArgvCandidatePath(params: {
return undefined;
}
// Reject wrapper targets carried through `$0 "$@"` because their trailing argv can
// widen execution semantics beyond the original approved command.
const carriedName = normalizeExecutableToken(carriedExecutable);
if (isDispatchWrapperExecutable(carriedName) || isShellWrapperExecutable(carriedName)) {
return undefined;
}
const resolution = resolveCommandResolutionFromArgv([carriedExecutable], params.cwd, params.env);
return resolveExecutionTargetCandidatePath(resolution, params.cwd);
}