mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-30 07:13:39 +00:00
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:
committed by
GitHub
parent
eabae023eb
commit
fc065b2693
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user