@@ -36,18 +36,18 @@ type ShellToken = string | { op: string } | { command: string }
3636 *
3737 * ## Command Processing Pipeline:
3838 *
39- * 1. **Subshell Detection**: Commands containing $() or `` are blocked if denylist exists
40- * 2. **Command Parsing**: Split chained commands (&&, ||, ;, |) into individual commands
41- * 3. **Pattern Matching**: For each command, find longest matching prefixes in both lists
42- * 4. **Decision Logic**: Apply longest prefix match rule to determine approval/denial
43- * 5. **Aggregation**: Combine decisions ( any denial blocks the entire command chain)
39+ * 1. **Subshell Detection**: Commands containing dangerous patterns like $(), ``, or (cmd1; cmd2) are flagged as security risks
40+ * 2. **Command Parsing**: Split chained commands (&&, ||, ;, |, & ) into individual commands for separate validation
41+ * 3. **Pattern Matching**: For each individual command, find the longest matching prefix in both allowlist and denylist
42+ * 4. **Decision Logic**: Apply longest prefix match rule - more specific (longer) matches take precedence
43+ * 5. **Aggregation**: Combine individual decisions - if any command is denied, the entire chain is denied
4444 *
4545 * ## Security Considerations:
4646 *
47- * - **Subshell Protection**: Prevents command injection via $( command), `command`, or process substitution
48- * - **Chain Analysis**: Each command in a chain (cmd1 && cmd2) is validated separately
49- * - **Case Insensitive**: All matching is case-insensitive for consistency
50- * - **Whitespace Handling**: Commands are trimmed and normalized before matching
47+ * - **Subshell Protection**: Detects and blocks command injection attempts via command substitution, process substitution, and subshell grouping
48+ * - **Chain Analysis**: Each command in a chain (cmd1 && cmd2) is validated separately to prevent bypassing via chaining
49+ * - **Case Insensitive**: All pattern matching is case-insensitive for consistent behavior across different input styles
50+ * - **Whitespace Handling**: Commands are trimmed and normalized before matching to prevent whitespace-based bypasses
5151 *
5252 * ## Configuration Merging:
5353 *
@@ -59,37 +59,73 @@ type ShellToken = string | { op: string } | { command: string }
5959 */
6060
6161/**
62- * Detect subshell usage and command substitution patterns:
63- * - $() - command substitution
64- * - `` - backticks (legacy command substitution)
65- * - <() - process substitution (input)
66- * - >() - process substitution (output)
67- * - $(()) - arithmetic expansion
68- * - $[] - arithmetic expansion (alternative syntax)
62+ * Detect subshell usage and command substitution patterns that could be security risks.
63+ *
64+ * Subshells allow executing commands in isolated environments and can be used to bypass
65+ * command validation by hiding dangerous commands inside substitution patterns.
66+ *
67+ * Detected patterns:
68+ * - $() - command substitution: executes command and substitutes output
69+ * - `` - backticks (legacy command substitution): same as $() but older syntax
70+ * - <() - process substitution (input): creates temporary file descriptor for command output
71+ * - >() - process substitution (output): creates temporary file descriptor for command input
72+ * - $(()) - arithmetic expansion: evaluates mathematical expressions (can contain commands)
73+ * - $[] - arithmetic expansion (alternative syntax): same as $(()) but older syntax
74+ * - (cmd1; cmd2) - subshell grouping: executes multiple commands in isolated subshell
75+ *
76+ * @param source - The command string to analyze for subshell patterns
77+ * @returns true if any subshell patterns are detected, false otherwise
6978 *
7079 * @example
7180 * ```typescript
72- * containsSubshell("echo $(date)") // true - command substitution
73- * containsSubshell("echo `date`") // true - backtick substitution
74- * containsSubshell("diff <(sort f1)") // true - process substitution
75- * containsSubshell("echo $((1+2))") // true - arithmetic expansion
76- * containsSubshell("echo $[1+2]") // true - arithmetic expansion (alt)
77- * containsSubshell("echo hello") // false - no subshells
81+ * // Command substitution - executes 'date' and substitutes its output
82+ * containsSubshell("echo $(date)") // true
83+ *
84+ * // Backtick substitution - legacy syntax for command substitution
85+ * containsSubshell("echo `date`") // true
86+ *
87+ * // Process substitution - creates file descriptor for command output
88+ * containsSubshell("diff <(sort f1)") // true
89+ *
90+ * // Arithmetic expansion - can contain command execution
91+ * containsSubshell("echo $((1+2))") // true
92+ * containsSubshell("echo $[1+2]") // true
93+ *
94+ * // Subshell grouping - executes commands in isolated environment
95+ * containsSubshell("(ls; rm file)") // true
96+ * containsSubshell("(cd /tmp && rm -rf *)") // true
97+ *
98+ * // Safe patterns that should NOT be flagged
99+ * containsSubshell("func(arg1, arg2)") // false - function call, not subshell
100+ * containsSubshell("echo hello") // false - no subshell patterns
101+ * containsSubshell("(simple text)") // false - no shell operators in parentheses
78102 * ```
79103 */
80104export function containsSubshell ( source : string ) : boolean {
81- return / ( \$ \( ) | ` | ( < \( | > \( ) | ( \$ \( \( ) | ( \$ \[ ) / . test ( source )
105+ // Check for command substitution, process substitution, and arithmetic expansion patterns
106+ // These patterns allow executing commands and substituting their output, which can bypass validation
107+ const commandSubstitutionPatterns = / ( \$ \( ) | ` | ( < \( | > \( ) | ( \$ \( \( ) | ( \$ \[ ) / . test ( source )
108+
109+ // Check for subshell grouping: parentheses containing shell command operators
110+ // Pattern explanation: \( = literal opening paren, [^)]* = any chars except closing paren,
111+ // [;&|]+ = one or more shell operators (semicolon, ampersand, pipe), [^)]* = any chars except closing paren, \) = literal closing paren
112+ // This detects dangerous patterns like: (cmd1; cmd2), (cmd1 && cmd2), (cmd1 || cmd2), (cmd1 | cmd2), (cmd1 & cmd2)
113+ // But avoids false positives like function calls: func(arg1, arg2) - no shell operators inside
114+ const subshellGroupingPattern = / \( [ ^ ) ] * [ ; & | ] + [ ^ ) ] * \) / . test ( source )
115+
116+ // Return true if any subshell pattern is detected
117+ return commandSubstitutionPatterns || subshellGroupingPattern
82118}
83119
84120/**
85121 * Split a command string into individual sub-commands by
86- * chaining operators (&&, ||, ;, or | ) and newlines.
122+ * chaining operators (&&, ||, ;, |, or & ) and newlines.
87123 *
88124 * Uses shell-quote to properly handle:
89125 * - Quoted strings (preserves quotes)
90126 * - Subshell commands ($(cmd), `cmd`, <(cmd), >(cmd))
91127 * - PowerShell redirections (2>&1)
92- * - Chain operators (&&, ||, ;, |)
128+ * - Chain operators (&&, ||, ;, |, & )
93129 * - Newlines as command separators
94130 */
95131export function parseCommand ( command : string ) : string [ ] {
@@ -228,7 +264,7 @@ function parseCommandLine(command: string): string[] {
228264
229265 // Simple fallback: split by common operators
230266 const fallbackCommands = processedCommand
231- . split ( / (?: & & | \| \| | ; | \| ) / )
267+ . split ( / (?: & & | \| \| | ; | \| | & ) / )
232268 . map ( ( cmd ) => cmd . trim ( ) )
233269 . filter ( ( cmd ) => cmd . length > 0 )
234270
@@ -253,13 +289,13 @@ function parseCommandLine(command: string): string[] {
253289 for ( const token of tokens ) {
254290 if ( typeof token === "object" && "op" in token ) {
255291 // Chain operator - split command
256- if ( [ "&&" , "||" , ";" , "|" ] . includes ( token . op ) ) {
292+ if ( [ "&&" , "||" , ";" , "|" , "&" ] . includes ( token . op ) ) {
257293 if ( currentCommand . length > 0 ) {
258294 commands . push ( currentCommand . join ( " " ) )
259295 currentCommand = [ ]
260296 }
261297 } else {
262- // Other operators (>, & ) are part of the command
298+ // Other operators (>) are part of the command
263299 currentCommand . push ( token . op )
264300 }
265301 } else if ( typeof token === "string" ) {
@@ -436,7 +472,7 @@ export type CommandDecision = "auto_approve" | "auto_deny" | "ask_user"
436472 *
437473 * **Decision Logic:**
438474 * 1. **Subshell Protection**: If subshells ($() or ``) are present and denylist exists → auto-deny
439- * 2. **Command Parsing**: Split command chains (&&, ||, ;, |) into individual commands
475+ * 2. **Command Parsing**: Split command chains (&&, ||, ;, |, & ) into individual commands
440476 * 3. **Individual Validation**: For each sub-command, apply longest prefix match rule
441477 * 4. **Aggregation**: Combine decisions using "any denial blocks all" principle
442478 *
0 commit comments