@@ -11,7 +11,7 @@ import {
1111 getSingleCommandDecision ,
1212 CommandValidator ,
1313 createCommandValidator ,
14- containsSubshell ,
14+ containsDangerousSubstitution ,
1515} from "../command-validation"
1616
1717describe ( "Command Validation" , ( ) => {
@@ -43,66 +43,6 @@ describe("Command Validation", () => {
4343 expect ( parseCommand ( "diff <(sort f1) <(sort f2)" ) ) . toEqual ( [ "diff" , "sort f1" , "sort f2" ] )
4444 } )
4545
46- it ( "detects additional subshell patterns" , ( ) => {
47- // Test $[] arithmetic expansion detection
48- expect ( parseCommand ( "echo $[1 + 2]" ) ) . toEqual ( [ "echo $[1 + 2]" ] )
49-
50- // Verify containsSubshell detects all subshell patterns
51- expect ( containsSubshell ( "echo $[1 + 2]" ) ) . toBe ( true ) // $[] arithmetic expansion
52- expect ( containsSubshell ( "echo $((1 + 2))" ) ) . toBe ( true ) // $(()) arithmetic expansion
53- expect ( containsSubshell ( "echo $(date)" ) ) . toBe ( true ) // $() command substitution
54- expect ( containsSubshell ( "echo `date`" ) ) . toBe ( true ) // backtick substitution
55- expect ( containsSubshell ( "diff <(sort f1) <(sort f2)" ) ) . toBe ( true ) // process substitution
56- expect ( containsSubshell ( "echo hello" ) ) . toBe ( false ) // no subshells
57- } )
58-
59- it ( "detects subshell grouping patterns" , ( ) => {
60- // Basic subshell grouping with shell operators
61- expect ( containsSubshell ( "(ls; rm file)" ) ) . toBe ( true )
62- expect ( containsSubshell ( "(cd /tmp && rm -rf *)" ) ) . toBe ( true )
63- expect ( containsSubshell ( "(command1 || command2)" ) ) . toBe ( true )
64- expect ( containsSubshell ( "(ls | grep test)" ) ) . toBe ( true )
65- expect ( containsSubshell ( "(sleep 10 & echo done)" ) ) . toBe ( true )
66-
67- // Nested subshells
68- expect ( containsSubshell ( "(cd /tmp && (rm -rf * || echo failed))" ) ) . toBe ( true )
69-
70- // Multiple operators in subshell
71- expect ( containsSubshell ( "(cmd1; cmd2 && cmd3 | cmd4)" ) ) . toBe ( true )
72-
73- // Subshell with spaces
74- expect ( containsSubshell ( "( ls ; rm file )" ) ) . toBe ( true )
75- } )
76-
77- it ( "does NOT detect legitimate parentheses usage" , ( ) => {
78- // Function calls should not be flagged as subshells
79- expect ( containsSubshell ( "myfunction(arg1, arg2)" ) ) . toBe ( false )
80- expect ( containsSubshell ( "func( arg1, arg2 )" ) ) . toBe ( false )
81-
82- // Simple parentheses without operators
83- expect ( containsSubshell ( "(simple text)" ) ) . toBe ( false )
84-
85- // Parentheses in strings
86- expect ( containsSubshell ( 'echo "this (has) parentheses"' ) ) . toBe ( false )
87-
88- // Empty parentheses
89- expect ( containsSubshell ( "()" ) ) . toBe ( false )
90- } )
91-
92- it ( "handles mixed subshell patterns" , ( ) => {
93- // Mixed subshell types
94- expect ( containsSubshell ( "(echo $(date); rm file)" ) ) . toBe ( true )
95-
96- // Subshell with command substitution
97- expect ( containsSubshell ( "(ls `pwd`; echo done)" ) ) . toBe ( true )
98-
99- // No subshells
100- expect ( containsSubshell ( "echo hello world" ) ) . toBe ( false )
101-
102- // Empty string
103- expect ( containsSubshell ( "" ) ) . toBe ( false )
104- } )
105-
10646 it ( "handles empty and whitespace input" , ( ) => {
10747 expect ( parseCommand ( "" ) ) . toEqual ( [ ] )
10848 expect ( parseCommand ( " " ) ) . toEqual ( [ ] )
@@ -261,6 +201,120 @@ ls -la || echo "Failed"`
261201 expect ( isAutoApprovedSingleCommand ( "npm test" , [ ] ) ) . toBe ( false )
262202 } )
263203 } )
204+
205+ describe ( "containsDangerousSubstitution" , ( ) => {
206+ it ( "detects parameter expansion with @P operator (prompt string expansion)" , ( ) => {
207+ // This is the specific vulnerability from the report - @P can execute commands
208+ expect ( containsDangerousSubstitution ( 'echo "${var1=aa\\140whoami\\140c}${var1@P}"' ) ) . toBe ( true )
209+ expect ( containsDangerousSubstitution ( "echo ${var@P}" ) ) . toBe ( true )
210+ expect ( containsDangerousSubstitution ( "result=${input@P}" ) ) . toBe ( true )
211+ expect ( containsDangerousSubstitution ( "${somevar@P}" ) ) . toBe ( true )
212+ } )
213+
214+ it ( "detects other dangerous parameter expansion operators" , ( ) => {
215+ // @Q - Quote removal
216+ expect ( containsDangerousSubstitution ( "echo ${var@Q}" ) ) . toBe ( true )
217+ // @E - Escape sequence expansion
218+ expect ( containsDangerousSubstitution ( "echo ${var@E}" ) ) . toBe ( true )
219+ // @A - Assignment statement
220+ expect ( containsDangerousSubstitution ( "echo ${var@A}" ) ) . toBe ( true )
221+ // @a - Attribute flags
222+ expect ( containsDangerousSubstitution ( "echo ${var@a}" ) ) . toBe ( true )
223+ } )
224+
225+ it ( "detects parameter assignments with octal escape sequences" , ( ) => {
226+ // Octal \140 is backtick, which can execute commands
227+ expect ( containsDangerousSubstitution ( 'echo "${var=\\140whoami\\140}"' ) ) . toBe ( true )
228+ expect ( containsDangerousSubstitution ( 'echo "${var:=\\140ls\\140}"' ) ) . toBe ( true )
229+ expect ( containsDangerousSubstitution ( 'echo "${var+\\140pwd\\140}"' ) ) . toBe ( true )
230+ expect ( containsDangerousSubstitution ( 'echo "${var:-\\140date\\140}"' ) ) . toBe ( true )
231+ expect ( containsDangerousSubstitution ( 'echo "${var:+\\140echo test\\140}"' ) ) . toBe ( true )
232+ expect ( containsDangerousSubstitution ( 'echo "${var:?\\140rm file\\140}"' ) ) . toBe ( true )
233+ // Test various octal patterns
234+ expect ( containsDangerousSubstitution ( 'echo "${var=\\001\\140\\141}"' ) ) . toBe ( true )
235+ expect ( containsDangerousSubstitution ( 'echo "${var=\\777}"' ) ) . toBe ( true )
236+ } )
237+
238+ it ( "detects parameter assignments with hex escape sequences" , ( ) => {
239+ // Hex \x60 is backtick
240+ expect ( containsDangerousSubstitution ( 'echo "${var=\\x60whoami\\x60}"' ) ) . toBe ( true )
241+ expect ( containsDangerousSubstitution ( 'echo "${var:=\\x60ls\\x60}"' ) ) . toBe ( true )
242+ expect ( containsDangerousSubstitution ( 'echo "${var+\\x60pwd\\x60}"' ) ) . toBe ( true )
243+ expect ( containsDangerousSubstitution ( 'echo "${var:-\\x60date\\x60}"' ) ) . toBe ( true )
244+ // Test various hex patterns
245+ expect ( containsDangerousSubstitution ( 'echo "${var=\\x00\\x60\\x61}"' ) ) . toBe ( true )
246+ expect ( containsDangerousSubstitution ( 'echo "${var=\\xFF}"' ) ) . toBe ( true )
247+ } )
248+
249+ it ( "detects parameter assignments with unicode escape sequences" , ( ) => {
250+ // Unicode \u0060 is backtick
251+ expect ( containsDangerousSubstitution ( 'echo "${var=\\u0060whoami\\u0060}"' ) ) . toBe ( true )
252+ expect ( containsDangerousSubstitution ( 'echo "${var:=\\u0060ls\\u0060}"' ) ) . toBe ( true )
253+ expect ( containsDangerousSubstitution ( 'echo "${var+\\u0060pwd\\u0060}"' ) ) . toBe ( true )
254+ expect ( containsDangerousSubstitution ( 'echo "${var:-\\u0060date\\u0060}"' ) ) . toBe ( true )
255+ // Test various unicode patterns
256+ expect ( containsDangerousSubstitution ( 'echo "${var=\\u0000\\u0060\\u0061}"' ) ) . toBe ( true )
257+ expect ( containsDangerousSubstitution ( 'echo "${var=\\uFFFF}"' ) ) . toBe ( true )
258+ } )
259+
260+ it ( "detects indirect variable references" , ( ) => {
261+ // ${!var} performs indirect expansion which can be dangerous
262+ expect ( containsDangerousSubstitution ( "echo ${!var}" ) ) . toBe ( true )
263+ expect ( containsDangerousSubstitution ( "result=${!indirect}" ) ) . toBe ( true )
264+ expect ( containsDangerousSubstitution ( "${!prefix*}" ) ) . toBe ( true )
265+ expect ( containsDangerousSubstitution ( "${!prefix@}" ) ) . toBe ( true )
266+ } )
267+
268+ it ( "detects here-strings with command substitution" , ( ) => {
269+ expect ( containsDangerousSubstitution ( "cat <<<$(whoami)" ) ) . toBe ( true )
270+ expect ( containsDangerousSubstitution ( "read <<<`date`" ) ) . toBe ( true )
271+ expect ( containsDangerousSubstitution ( "grep pattern <<< $(ls)" ) ) . toBe ( true )
272+ expect ( containsDangerousSubstitution ( "sort <<< `pwd`" ) ) . toBe ( true )
273+ } )
274+
275+ it ( "does NOT flag safe parameter expansions" , ( ) => {
276+ // Regular parameter expansions without dangerous operators
277+ expect ( containsDangerousSubstitution ( "echo ${var}" ) ) . toBe ( false )
278+ expect ( containsDangerousSubstitution ( "echo ${var:-default}" ) ) . toBe ( false )
279+ expect ( containsDangerousSubstitution ( "echo ${var:+alternative}" ) ) . toBe ( false )
280+ expect ( containsDangerousSubstitution ( "echo ${#var}" ) ) . toBe ( false )
281+ expect ( containsDangerousSubstitution ( "echo ${var%pattern}" ) ) . toBe ( false )
282+ expect ( containsDangerousSubstitution ( "echo ${var#pattern}" ) ) . toBe ( false )
283+ expect ( containsDangerousSubstitution ( "echo ${var/old/new}" ) ) . toBe ( false )
284+ expect ( containsDangerousSubstitution ( "echo ${var^^}" ) ) . toBe ( false )
285+ expect ( containsDangerousSubstitution ( "echo ${var,,}" ) ) . toBe ( false )
286+ expect ( containsDangerousSubstitution ( "echo ${var:0:5}" ) ) . toBe ( false )
287+
288+ // Parameter assignments without escape sequences
289+ expect ( containsDangerousSubstitution ( 'echo "${var=normal text}"' ) ) . toBe ( false )
290+ expect ( containsDangerousSubstitution ( 'echo "${var:-default value}"' ) ) . toBe ( false )
291+ expect ( containsDangerousSubstitution ( 'echo "${var:+alternative}"' ) ) . toBe ( false )
292+
293+ // Here-strings without command substitution
294+ expect ( containsDangerousSubstitution ( "cat <<<plain_text" ) ) . toBe ( false )
295+ expect ( containsDangerousSubstitution ( 'read <<<"static string"' ) ) . toBe ( false )
296+ expect ( containsDangerousSubstitution ( "grep <<<$var" ) ) . toBe ( false ) // Plain variable, not command substitution
297+ } )
298+
299+ it ( "handles complex combinations of dangerous patterns" , ( ) => {
300+ // Multiple dangerous patterns in one command
301+ expect ( containsDangerousSubstitution ( 'echo "${var1=\\140ls\\140}${var1@P}" && ${!indirect}' ) ) . toBe ( true )
302+ // Nested patterns
303+ expect ( containsDangerousSubstitution ( 'echo "${outer=${inner@P}}"' ) ) . toBe ( true )
304+ // Mixed with safe patterns
305+ expect ( containsDangerousSubstitution ( "echo ${safe:-default} ${dangerous@P}" ) ) . toBe ( true )
306+ } )
307+
308+ it ( "detects the exact exploit from the security report" , ( ) => {
309+ // The exact pattern reported in the vulnerability
310+ const exploit = 'echo "${var1=aa\\140whoami\\140c}${var1@P}"'
311+ expect ( containsDangerousSubstitution ( exploit ) ) . toBe ( true )
312+
313+ // Variations of the exploit
314+ expect ( containsDangerousSubstitution ( 'echo "${x=\\140id\\140}${x@P}"' ) ) . toBe ( true )
315+ expect ( containsDangerousSubstitution ( 'result="${cmd=\\x60pwd\\x60}${cmd@P}"' ) ) . toBe ( true )
316+ } )
317+ } )
264318} )
265319
266320/**
@@ -771,6 +825,97 @@ describe("Unified Command Decision Functions", () => {
771825 expect ( getCommandDecision ( "npm install && echo done" , allowedCommands , deniedCommands ) ) . toBe ( "auto_approve" )
772826 } )
773827
828+ describe ( "dangerous substitution handling" , ( ) => {
829+ it ( "prevents auto-approve for commands with dangerous parameter expansion" , ( ) => {
830+ // Commands that would normally be auto-approved are blocked by dangerous patterns
831+ expect ( getCommandDecision ( "echo ${var@P}" , allowedCommands , deniedCommands ) ) . toBe ( "ask_user" )
832+ expect ( getCommandDecision ( "echo hello" , allowedCommands , deniedCommands ) ) . toBe ( "auto_approve" ) // Safe version
833+
834+ // Even with allowed prefix, dangerous patterns prevent auto-approval
835+ expect ( getCommandDecision ( "npm install ${var@P}" , allowedCommands , deniedCommands ) ) . toBe ( "ask_user" )
836+ expect (
837+ getCommandDecision ( 'echo "${var1=\\140whoami\\140c}${var1@P}"' , allowedCommands , deniedCommands ) ,
838+ ) . toBe ( "ask_user" )
839+ } )
840+
841+ it ( "does NOT override auto_deny decisions with dangerous patterns" , ( ) => {
842+ // If a command would be denied, dangerous patterns don't change that
843+ expect ( getCommandDecision ( "npm test ${var@P}" , allowedCommands , deniedCommands ) ) . toBe ( "auto_deny" )
844+ expect ( getCommandDecision ( 'npm test "${var=\\140ls\\140}"' , allowedCommands , deniedCommands ) ) . toBe (
845+ "auto_deny" ,
846+ )
847+
848+ // Regular denied commands without dangerous patterns
849+ expect ( getCommandDecision ( "npm test --coverage" , allowedCommands , deniedCommands ) ) . toBe ( "auto_deny" )
850+ } )
851+
852+ it ( "prevents auto-approval for various dangerous substitution types" , ( ) => {
853+ // Octal escape sequences
854+ expect ( getCommandDecision ( 'echo "${var=\\140ls\\140}"' , allowedCommands , deniedCommands ) ) . toBe (
855+ "ask_user" ,
856+ )
857+ expect ( getCommandDecision ( 'npm run "${var:=\\140pwd\\140}"' , allowedCommands , deniedCommands ) ) . toBe (
858+ "ask_user" ,
859+ )
860+
861+ // Hex escape sequences
862+ expect ( getCommandDecision ( 'echo "${var=\\x60whoami\\x60}"' , allowedCommands , deniedCommands ) ) . toBe (
863+ "ask_user" ,
864+ )
865+
866+ // Indirect variable references
867+ expect ( getCommandDecision ( "echo ${!var}" , allowedCommands , deniedCommands ) ) . toBe ( "ask_user" )
868+
869+ // Here-strings with command substitution
870+ expect ( getCommandDecision ( "cat <<<$(whoami)" , allowedCommands , deniedCommands ) ) . toBe ( "ask_user" )
871+ expect ( getCommandDecision ( "read <<<`date`" , allowedCommands , deniedCommands ) ) . toBe ( "ask_user" )
872+ } )
873+
874+ it ( "allows safe parameter expansions to follow normal rules" , ( ) => {
875+ // Safe parameter expansions should follow normal allowlist/denylist rules
876+ expect ( getCommandDecision ( "echo ${var}" , allowedCommands , deniedCommands ) ) . toBe ( "auto_approve" )
877+ expect ( getCommandDecision ( "echo ${var:-default}" , allowedCommands , deniedCommands ) ) . toBe ( "auto_approve" )
878+ expect ( getCommandDecision ( "npm install ${package_name}" , allowedCommands , deniedCommands ) ) . toBe (
879+ "auto_approve" ,
880+ )
881+
882+ // Here-strings without command substitution are safe
883+ expect ( getCommandDecision ( "echo test <<<$var" , allowedCommands , deniedCommands ) ) . toBe ( "auto_approve" )
884+ } )
885+
886+ it ( "handles command chains correctly with dangerous patterns" , ( ) => {
887+ // If any part of a chain has dangerous substitution, prevent auto-approval
888+ expect ( getCommandDecision ( "npm install && echo ${var@P}" , allowedCommands , deniedCommands ) ) . toBe (
889+ "ask_user" ,
890+ )
891+ expect (
892+ getCommandDecision ( 'echo safe && echo "${var=\\140ls\\140}"' , allowedCommands , deniedCommands ) ,
893+ ) . toBe ( "ask_user" )
894+
895+ // But if chain would be denied, keep the deny decision
896+ expect ( getCommandDecision ( "npm test ${var@P} && echo safe" , allowedCommands , deniedCommands ) ) . toBe (
897+ "auto_deny" ,
898+ )
899+ expect ( getCommandDecision ( "npm install && npm test ${var@P}" , allowedCommands , deniedCommands ) ) . toBe (
900+ "auto_deny" ,
901+ )
902+
903+ // Safe chains should still be auto-approved
904+ expect ( getCommandDecision ( "npm install && echo done" , allowedCommands , deniedCommands ) ) . toBe (
905+ "auto_approve" ,
906+ )
907+ } )
908+
909+ it ( "handles the exact exploit from the security report" , ( ) => {
910+ const exploit = 'echo "${var1=aa\\140whoami\\140c}${var1@P}"'
911+ // Even though 'echo' is in the allowlist, the dangerous pattern prevents auto-approval
912+ expect ( getCommandDecision ( exploit , allowedCommands , deniedCommands ) ) . toBe ( "ask_user" )
913+
914+ // But if it were a denied command, it would still be denied
915+ expect ( getCommandDecision ( `npm test ${ exploit } ` , allowedCommands , deniedCommands ) ) . toBe ( "auto_deny" )
916+ } )
917+ } )
918+
774919 it ( "returns auto_deny for commands with any sub-command auto-denied" , ( ) => {
775920 expect ( getCommandDecision ( "npm test" , allowedCommands , deniedCommands ) ) . toBe ( "auto_deny" )
776921 expect ( getCommandDecision ( "npm install && npm test" , allowedCommands , deniedCommands ) ) . toBe ( "auto_deny" )
@@ -899,7 +1044,6 @@ describe("Unified Command Decision Functions", () => {
8991044
9001045 expect ( details . decision ) . toBe ( "auto_approve" )
9011046 expect ( details . subCommands ) . toEqual ( [ "npm install" , "echo done" ] )
902- expect ( details . hasSubshells ) . toBe ( false )
9031047 expect ( details . allowedMatches ) . toHaveLength ( 2 )
9041048 expect ( details . deniedMatches ) . toHaveLength ( 2 )
9051049
@@ -912,12 +1056,10 @@ describe("Unified Command Decision Functions", () => {
9121056
9131057 it ( "detects subshells correctly" , ( ) => {
9141058 const details = validator . getValidationDetails ( "npm install $(echo test)" )
915- expect ( details . hasSubshells ) . toBe ( true )
9161059 expect ( details . decision ) . toBe ( "auto_approve" ) // all commands are allowed
9171060
9181061 // Test with denied prefix in subshell
9191062 const detailsWithDenied = validator . getValidationDetails ( "npm install $(npm test)" )
920- expect ( detailsWithDenied . hasSubshells ) . toBe ( true )
9211063 expect ( detailsWithDenied . decision ) . toBe ( "auto_deny" ) // npm test is denied
9221064 } )
9231065
0 commit comments