fix: harden allow-always shell multiplexer wrapper handling

This commit is contained in:
Peter Steinberger
2026-02-24 03:06:34 +00:00
parent 4a3f8438e5
commit a67689a7e3
8 changed files with 193 additions and 1 deletions

View File

@@ -7,6 +7,7 @@ const WINDOWS_EXE_SUFFIX = ".exe";
const POSIX_SHELL_WRAPPER_NAMES = ["ash", "bash", "dash", "fish", "ksh", "sh", "zsh"] as const;
const WINDOWS_CMD_WRAPPER_NAMES = ["cmd"] as const;
const POWERSHELL_WRAPPER_NAMES = ["powershell", "pwsh"] as const;
const SHELL_MULTIPLEXER_WRAPPER_NAMES = ["busybox", "toybox"] as const;
const DISPATCH_WRAPPER_NAMES = [
"chrt",
"doas",
@@ -42,6 +43,7 @@ export const DISPATCH_WRAPPER_EXECUTABLES = new Set(withWindowsExeAliases(DISPAT
const POSIX_SHELL_WRAPPER_CANONICAL = new Set<string>(POSIX_SHELL_WRAPPER_NAMES);
const WINDOWS_CMD_WRAPPER_CANONICAL = new Set<string>(WINDOWS_CMD_WRAPPER_NAMES);
const POWERSHELL_WRAPPER_CANONICAL = new Set<string>(POWERSHELL_WRAPPER_NAMES);
const SHELL_MULTIPLEXER_WRAPPER_CANONICAL = new Set<string>(SHELL_MULTIPLEXER_WRAPPER_NAMES);
const DISPATCH_WRAPPER_CANONICAL = new Set<string>(DISPATCH_WRAPPER_NAMES);
const SHELL_WRAPPER_CANONICAL = new Set<string>([
...POSIX_SHELL_WRAPPER_NAMES,
@@ -133,6 +135,39 @@ function findShellWrapperSpec(baseExecutable: string): ShellWrapperSpec | null {
return null;
}
export type ShellMultiplexerUnwrapResult =
| { kind: "not-wrapper" }
| { kind: "blocked"; wrapper: string }
| { kind: "unwrapped"; wrapper: string; argv: string[] };
export function unwrapKnownShellMultiplexerInvocation(
argv: string[],
): ShellMultiplexerUnwrapResult {
const token0 = argv[0]?.trim();
if (!token0) {
return { kind: "not-wrapper" };
}
const wrapper = normalizeExecutableToken(token0);
if (!SHELL_MULTIPLEXER_WRAPPER_CANONICAL.has(wrapper)) {
return { kind: "not-wrapper" };
}
let appletIndex = 1;
if (argv[appletIndex]?.trim() === "--") {
appletIndex += 1;
}
const applet = argv[appletIndex]?.trim();
if (!applet || !isShellWrapperExecutable(applet)) {
return { kind: "blocked", wrapper };
}
const unwrapped = argv.slice(appletIndex);
if (unwrapped.length === 0) {
return { kind: "blocked", wrapper };
}
return { kind: "unwrapped", wrapper, argv: unwrapped };
}
export function isEnvAssignment(token: string): boolean {
return /^[A-Za-z_][A-Za-z0-9_]*=.*/.test(token);
}
@@ -474,6 +509,18 @@ function hasEnvManipulationBeforeShellWrapperInternal(
);
}
const shellMultiplexerUnwrap = unwrapKnownShellMultiplexerInvocation(argv);
if (shellMultiplexerUnwrap.kind === "blocked") {
return false;
}
if (shellMultiplexerUnwrap.kind === "unwrapped") {
return hasEnvManipulationBeforeShellWrapperInternal(
shellMultiplexerUnwrap.argv,
depth + 1,
envManipulationSeen,
);
}
const wrapper = findShellWrapperSpec(normalizeExecutableToken(token0));
if (!wrapper) {
return false;
@@ -577,6 +624,14 @@ function extractShellWrapperCommandInternal(
return extractShellWrapperCommandInternal(dispatchUnwrap.argv, rawCommand, depth + 1);
}
const shellMultiplexerUnwrap = unwrapKnownShellMultiplexerInvocation(argv);
if (shellMultiplexerUnwrap.kind === "blocked") {
return { isWrapper: false, command: null };
}
if (shellMultiplexerUnwrap.kind === "unwrapped") {
return extractShellWrapperCommandInternal(shellMultiplexerUnwrap.argv, rawCommand, depth + 1);
}
const base0 = normalizeExecutableToken(token0);
const wrapper = findShellWrapperSpec(base0);
if (!wrapper) {