Harden macOS shell wrapper allowlist parsing [AI] (#78518)

* fix: harden shell wrapper allowlist parsing

* fix: harden shell wrapper approval binding

* docs: add changelog entry for PR merge

---------

Co-authored-by: Ishaan <ishaan@Ishaans-Mac-mini.local>
This commit is contained in:
Pavan Kumar Gondhi
2026-05-08 10:18:41 +05:30
committed by GitHub
parent eabae023eb
commit fc065b2693
23 changed files with 1200 additions and 204 deletions

View File

@@ -12,35 +12,212 @@ export const POWERSHELL_INLINE_COMMAND_FLAGS = new Set([
"-e",
]);
const POSIX_SHELL_OPTIONS_WITH_SEPARATE_VALUES = new Set([
"--init-file",
"--rcfile",
"-O",
"-o",
"+O",
"+o",
]);
function isCombinedCommandFlag(token: string): boolean {
return parseCombinedCommandFlag(token) !== null;
}
function parseCombinedCommandFlag(
token: string,
): { attachedCommand: string | null; separateValueCount: number } | null {
if (token.length < 2 || token[0] !== "-" || token[1] === "-") {
return null;
}
const optionChars = token.slice(1);
const commandFlagIndex = optionChars.indexOf("c");
if (commandFlagIndex === -1 || optionChars.includes("-")) {
return null;
}
const suffix = optionChars.slice(commandFlagIndex + 1);
if (suffix && !/^[A-Za-z]+$/.test(suffix)) {
return { attachedCommand: suffix, separateValueCount: 0 };
}
return {
attachedCommand: null,
separateValueCount: [...optionChars].filter((char) => char === "o" || char === "O").length,
};
}
function combinedSeparateValueOptionCount(token: string): number {
if (
token.length < 2 ||
(token[0] !== "-" && token[0] !== "+") ||
token[1] === "-" ||
token.slice(1).includes("-")
) {
return 0;
}
return [...token.slice(1)].filter((char) => char === "o" || char === "O").length;
}
function consumesSeparateValue(token: string): boolean {
return POSIX_SHELL_OPTIONS_WITH_SEPARATE_VALUES.has(token);
}
function isPosixInteractiveModeOption(token: string): boolean {
return token === "--interactive" || isPosixShortOption(token, "i");
}
function isPosixShortOption(token: string, option: string): boolean {
if (token.length < 2 || token[0] !== "-" || token[1] === "-") {
return false;
}
const optionChars = token.slice(1);
return !optionChars.includes("-") && optionChars.includes(option);
}
function advancePosixInlineOptionScan(token: string): number {
const combinedValueCount = combinedSeparateValueOptionCount(token);
if (combinedValueCount > 0) {
return 1 + combinedValueCount;
}
if (consumesSeparateValue(token)) {
return 2;
}
return 1;
}
export function resolveInlineCommandMatch(
argv: string[],
flags: ReadonlySet<string>,
options: { allowCombinedC?: boolean } = {},
): { command: string | null; valueTokenIndex: number | null } {
for (let i = 1; i < argv.length; i += 1) {
for (let i = 1; i < argv.length; ) {
const token = argv[i]?.trim();
if (!token) {
i += 1;
continue;
}
const lower = normalizeLowercaseStringOrEmpty(token);
if (lower === "--") {
break;
}
if (flags.has(lower)) {
const comparableToken = options.allowCombinedC ? token : lower;
if (flags.has(comparableToken)) {
const valueTokenIndex = i + 1 < argv.length ? i + 1 : null;
const command = argv[i + 1]?.trim();
return { command: command ? command : null, valueTokenIndex };
}
if (options.allowCombinedC && /^-[^-]*c[^-]*$/i.test(token)) {
const commandIndex = lower.indexOf("c");
const inline = token.slice(commandIndex + 1).trim();
if (inline) {
return { command: inline, valueTokenIndex: i };
if (options.allowCombinedC && isCombinedCommandFlag(token)) {
const combined = parseCombinedCommandFlag(token);
if (combined?.attachedCommand != null) {
return { command: combined.attachedCommand.trim() || null, valueTokenIndex: i };
}
const valueTokenIndex = i + 1 < argv.length ? i + 1 : null;
const command = argv[i + 1]?.trim();
const valueTokenIndex = i + 1 + (combined?.separateValueCount ?? 0);
const command = argv[valueTokenIndex]?.trim();
return { command: command ? command : null, valueTokenIndex };
}
if (options.allowCombinedC && !token.startsWith("-") && !token.startsWith("+")) {
break;
}
i += options.allowCombinedC ? advancePosixInlineOptionScan(token) : 1;
}
return { command: null, valueTokenIndex: null };
}
export function hasPosixInteractiveStartupBeforeInlineCommand(
argv: string[],
flags: ReadonlySet<string>,
): boolean {
let sawInteractiveMode = false;
for (let i = 1; i < argv.length; ) {
const token = argv[i]?.trim();
if (!token) {
i += 1;
continue;
}
if (token === "--") {
return false;
}
if (isPosixInteractiveModeOption(token)) {
sawInteractiveMode = true;
}
if (flags.has(token) || isCombinedCommandFlag(token)) {
return sawInteractiveMode;
}
if (!token.startsWith("-") && !token.startsWith("+")) {
return false;
}
i += advancePosixInlineOptionScan(token);
}
return false;
}
export function hasPosixLoginStartupBeforeInlineCommand(
argv: string[],
flags: ReadonlySet<string>,
): boolean {
let sawLoginMode = false;
for (let i = 1; i < argv.length; ) {
const token = argv[i]?.trim();
if (!token) {
i += 1;
continue;
}
if (token === "--") {
return false;
}
if (token === "--login" || isPosixShortOption(token, "l")) {
sawLoginMode = true;
}
if (flags.has(token) || isCombinedCommandFlag(token)) {
return sawLoginMode;
}
if (!token.startsWith("-") && !token.startsWith("+")) {
return false;
}
i += advancePosixInlineOptionScan(token);
}
return false;
}
export function hasFishInitCommandOption(argv: string[]): boolean {
for (let i = 1; i < argv.length; i += 1) {
const token = argv[i]?.trim();
if (!token) {
continue;
}
if (token === "--") {
return false;
}
if (
token === "-C" ||
token === "--init-command" ||
(token.startsWith("-C") && token !== "-C") ||
token.startsWith("--init-command=")
) {
return true;
}
if (!token.startsWith("-") && !token.startsWith("+")) {
return false;
}
}
return false;
}
export function hasFishAttachedCommandOption(argv: string[]): boolean {
for (let i = 1; i < argv.length; i += 1) {
const token = argv[i]?.trim();
if (!token) {
continue;
}
if (token === "--") {
return false;
}
if (token.startsWith("-c") && token !== "-c") {
return true;
}
if (!token.startsWith("-") && !token.startsWith("+")) {
return false;
}
}
return false;
}