/** * Shell argument escaping utilities for Windows cmd.exe and PowerShell. * * These functions handle escaping command line arguments to prevent injection * and ensure proper argument passing when spawning processes via shell. * * References: * - cmd.exe escaping: https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/cmd * - PowerShell escaping: https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules * * For production use, consider using the 'shescape' npm package which provides * more comprehensive cross-platform shell escaping with extensive testing. */ /** * Characters that require quoting in cmd.exe. * Based on cmd.exe special characters: https://ss64.com/nt/syntax-esc.html */ const CMD_SPECIAL_CHARS = /[ &|<>^%!()"\n\r#?*]/; /** * Characters that require quoting in PowerShell. * Based on PowerShell special characters: https://ss64.com/ps/syntax-esc.html */ const POWERSHELL_SPECIAL_CHARS = /[ &|<>^%!()"\n\r#?*`$@{}[\]';,]/; /** * Escape a single argument for use in cmd.exe. * * Strategy: * 1. If the argument contains special characters or is long, wrap in double quotes * 2. Escape existing double quotes by doubling them * 3. Escape carets (^) as they're the escape character in cmd.exe * * @param arg - The argument to escape * @returns The escaped argument safe for cmd.exe */ export function escapeCmdArg(arg: string): string { // If no special characters and not too long, return as-is if (!CMD_SPECIAL_CHARS.test(arg) && arg.length <= 100) { return arg; } // Escape double quotes by doubling them, and carets by doubling const escaped = arg.replace(/"/g, '""').replace(/\^/g, '^^'); // Wrap in double quotes return `"${escaped}"`; } /** * Escape a single argument for use in PowerShell. * * Strategy: * 1. If the argument contains special characters or is long, wrap in single quotes * 2. Escape existing single quotes by doubling them (PowerShell's single-quote escape) * * Single quotes in PowerShell treat the content as a literal string, which is * safer than double quotes (which allow variable expansion). * * @param arg - The argument to escape * @returns The escaped argument safe for PowerShell */ export function escapePowerShellArg(arg: string): string { // If no special characters and not too long, return as-is if (!POWERSHELL_SPECIAL_CHARS.test(arg) && arg.length <= 100) { return arg; } // Escape single quotes by doubling them (PowerShell's escape mechanism) const escaped = arg.replace(/'/g, "''"); // Wrap in single quotes (prevents variable expansion) return `'${escaped}'`; } /** * Escape an array of arguments for use in cmd.exe. * * @param args - The arguments to escape * @returns The escaped arguments safe for cmd.exe */ export function escapeCmdArgs(args: string[]): string[] { return args.map(escapeCmdArg); } /** * Escape an array of arguments for use in PowerShell. * * @param args - The arguments to escape * @returns The escaped arguments safe for PowerShell */ export function escapePowerShellArgs(args: string[]): string[] { return args.map(escapePowerShellArg); } /** * Detect if a shell path refers to PowerShell. * * @param shellPath - The shell path to check * @returns True if the shell is PowerShell (either Windows PowerShell or PowerShell Core) */ export function isPowerShellShell(shellPath: string | undefined): boolean { if (!shellPath) return false; const lower = shellPath.toLowerCase(); return lower.includes('powershell') || lower.includes('pwsh'); } /** * Escape arguments based on the target shell. * * @param args - The arguments to escape * @param shell - The shell path or name (optional, defaults to cmd.exe behavior) * @returns The escaped arguments */ export function escapeArgsForShell(args: string[], shell?: string): string[] { if (isPowerShellShell(shell)) { return escapePowerShellArgs(args); } return escapeCmdArgs(args); }