@@ -11,6 +11,7 @@ import {
1111 getSingleCommandDecision ,
1212 CommandValidator ,
1313 createCommandValidator ,
14+ containsDangerousSubstitution ,
1415} from "../command-validation"
1516
1617describe ( "Command Validation" , ( ) => {
@@ -200,6 +201,109 @@ ls -la || echo "Failed"`
200201 expect ( isAutoApprovedSingleCommand ( "npm test" , [ ] ) ) . toBe ( false )
201202 } )
202203 } )
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 indirect variable references" , ( ) => {
250+ // ${!var} performs indirect expansion which can be dangerous
251+ expect ( containsDangerousSubstitution ( "echo ${!var}" ) ) . toBe ( true )
252+ expect ( containsDangerousSubstitution ( "result=${!indirect}" ) ) . toBe ( true )
253+ expect ( containsDangerousSubstitution ( "${!prefix*}" ) ) . toBe ( true )
254+ expect ( containsDangerousSubstitution ( "${!prefix@}" ) ) . toBe ( true )
255+ } )
256+
257+ it ( "detects here-strings with command substitution" , ( ) => {
258+ expect ( containsDangerousSubstitution ( "cat <<<$(whoami)" ) ) . toBe ( true )
259+ expect ( containsDangerousSubstitution ( "read <<<`date`" ) ) . toBe ( true )
260+ expect ( containsDangerousSubstitution ( "grep pattern <<< $(ls)" ) ) . toBe ( true )
261+ expect ( containsDangerousSubstitution ( "sort <<< `pwd`" ) ) . toBe ( true )
262+ } )
263+
264+ it ( "does NOT flag safe parameter expansions" , ( ) => {
265+ // Regular parameter expansions without dangerous operators
266+ expect ( containsDangerousSubstitution ( "echo ${var}" ) ) . toBe ( false )
267+ expect ( containsDangerousSubstitution ( "echo ${var:-default}" ) ) . toBe ( false )
268+ expect ( containsDangerousSubstitution ( "echo ${var:+alternative}" ) ) . toBe ( false )
269+ expect ( containsDangerousSubstitution ( "echo ${#var}" ) ) . toBe ( false )
270+ expect ( containsDangerousSubstitution ( "echo ${var%pattern}" ) ) . toBe ( false )
271+ expect ( containsDangerousSubstitution ( "echo ${var#pattern}" ) ) . toBe ( false )
272+ expect ( containsDangerousSubstitution ( "echo ${var/old/new}" ) ) . toBe ( false )
273+ expect ( containsDangerousSubstitution ( "echo ${var^^}" ) ) . toBe ( false )
274+ expect ( containsDangerousSubstitution ( "echo ${var,,}" ) ) . toBe ( false )
275+ expect ( containsDangerousSubstitution ( "echo ${var:0:5}" ) ) . toBe ( false )
276+
277+ // Parameter assignments without escape sequences
278+ expect ( containsDangerousSubstitution ( 'echo "${var=normal text}"' ) ) . toBe ( false )
279+ expect ( containsDangerousSubstitution ( 'echo "${var:-default value}"' ) ) . toBe ( false )
280+ expect ( containsDangerousSubstitution ( 'echo "${var:+alternative}"' ) ) . toBe ( false )
281+
282+ // Here-strings without command substitution
283+ expect ( containsDangerousSubstitution ( "cat <<<plain_text" ) ) . toBe ( false )
284+ expect ( containsDangerousSubstitution ( 'read <<<"static string"' ) ) . toBe ( false )
285+ expect ( containsDangerousSubstitution ( "grep <<<$var" ) ) . toBe ( false ) // Plain variable, not command substitution
286+ } )
287+
288+ it ( "handles complex combinations of dangerous patterns" , ( ) => {
289+ // Multiple dangerous patterns in one command
290+ expect ( containsDangerousSubstitution ( 'echo "${var1=\\140ls\\140}${var1@P}" && ${!indirect}' ) ) . toBe ( true )
291+ // Nested patterns
292+ expect ( containsDangerousSubstitution ( 'echo "${outer=${inner@P}}"' ) ) . toBe ( true )
293+ // Mixed with safe patterns
294+ expect ( containsDangerousSubstitution ( "echo ${safe:-default} ${dangerous@P}" ) ) . toBe ( true )
295+ } )
296+
297+ it ( "detects the exact exploit from the security report" , ( ) => {
298+ // The exact pattern reported in the vulnerability
299+ const exploit = 'echo "${var1=aa\\140whoami\\140c}${var1@P}"'
300+ expect ( containsDangerousSubstitution ( exploit ) ) . toBe ( true )
301+
302+ // Variations of the exploit
303+ expect ( containsDangerousSubstitution ( 'echo "${x=\\140id\\140}${x@P}"' ) ) . toBe ( true )
304+ expect ( containsDangerousSubstitution ( 'result="${cmd=\\x60pwd\\x60}${cmd@P}"' ) ) . toBe ( true )
305+ } )
306+ } )
203307} )
204308
205309/**
@@ -710,6 +814,97 @@ describe("Unified Command Decision Functions", () => {
710814 expect ( getCommandDecision ( "npm install && echo done" , allowedCommands , deniedCommands ) ) . toBe ( "auto_approve" )
711815 } )
712816
817+ describe ( "dangerous substitution handling" , ( ) => {
818+ it ( "prevents auto-approve for commands with dangerous parameter expansion" , ( ) => {
819+ // Commands that would normally be auto-approved are blocked by dangerous patterns
820+ expect ( getCommandDecision ( "echo ${var@P}" , allowedCommands , deniedCommands ) ) . toBe ( "ask_user" )
821+ expect ( getCommandDecision ( "echo hello" , allowedCommands , deniedCommands ) ) . toBe ( "auto_approve" ) // Safe version
822+
823+ // Even with allowed prefix, dangerous patterns prevent auto-approval
824+ expect ( getCommandDecision ( "npm install ${var@P}" , allowedCommands , deniedCommands ) ) . toBe ( "ask_user" )
825+ expect (
826+ getCommandDecision ( 'echo "${var1=\\140whoami\\140c}${var1@P}"' , allowedCommands , deniedCommands ) ,
827+ ) . toBe ( "ask_user" )
828+ } )
829+
830+ it ( "does NOT override auto_deny decisions with dangerous patterns" , ( ) => {
831+ // If a command would be denied, dangerous patterns don't change that
832+ expect ( getCommandDecision ( "npm test ${var@P}" , allowedCommands , deniedCommands ) ) . toBe ( "auto_deny" )
833+ expect ( getCommandDecision ( 'npm test "${var=\\140ls\\140}"' , allowedCommands , deniedCommands ) ) . toBe (
834+ "auto_deny" ,
835+ )
836+
837+ // Regular denied commands without dangerous patterns
838+ expect ( getCommandDecision ( "npm test --coverage" , allowedCommands , deniedCommands ) ) . toBe ( "auto_deny" )
839+ } )
840+
841+ it ( "prevents auto-approval for various dangerous substitution types" , ( ) => {
842+ // Octal escape sequences
843+ expect ( getCommandDecision ( 'echo "${var=\\140ls\\140}"' , allowedCommands , deniedCommands ) ) . toBe (
844+ "ask_user" ,
845+ )
846+ expect ( getCommandDecision ( 'npm run "${var:=\\140pwd\\140}"' , allowedCommands , deniedCommands ) ) . toBe (
847+ "ask_user" ,
848+ )
849+
850+ // Hex escape sequences
851+ expect ( getCommandDecision ( 'echo "${var=\\x60whoami\\x60}"' , allowedCommands , deniedCommands ) ) . toBe (
852+ "ask_user" ,
853+ )
854+
855+ // Indirect variable references
856+ expect ( getCommandDecision ( "echo ${!var}" , allowedCommands , deniedCommands ) ) . toBe ( "ask_user" )
857+
858+ // Here-strings with command substitution
859+ expect ( getCommandDecision ( "cat <<<$(whoami)" , allowedCommands , deniedCommands ) ) . toBe ( "ask_user" )
860+ expect ( getCommandDecision ( "read <<<`date`" , allowedCommands , deniedCommands ) ) . toBe ( "ask_user" )
861+ } )
862+
863+ it ( "allows safe parameter expansions to follow normal rules" , ( ) => {
864+ // Safe parameter expansions should follow normal allowlist/denylist rules
865+ expect ( getCommandDecision ( "echo ${var}" , allowedCommands , deniedCommands ) ) . toBe ( "auto_approve" )
866+ expect ( getCommandDecision ( "echo ${var:-default}" , allowedCommands , deniedCommands ) ) . toBe ( "auto_approve" )
867+ expect ( getCommandDecision ( "npm install ${package_name}" , allowedCommands , deniedCommands ) ) . toBe (
868+ "auto_approve" ,
869+ )
870+
871+ // Here-strings without command substitution are safe
872+ expect ( getCommandDecision ( "echo test <<<$var" , allowedCommands , deniedCommands ) ) . toBe ( "auto_approve" )
873+ } )
874+
875+ it ( "handles command chains correctly with dangerous patterns" , ( ) => {
876+ // If any part of a chain has dangerous substitution, prevent auto-approval
877+ expect ( getCommandDecision ( "npm install && echo ${var@P}" , allowedCommands , deniedCommands ) ) . toBe (
878+ "ask_user" ,
879+ )
880+ expect (
881+ getCommandDecision ( 'echo safe && echo "${var=\\140ls\\140}"' , allowedCommands , deniedCommands ) ,
882+ ) . toBe ( "ask_user" )
883+
884+ // But if chain would be denied, keep the deny decision
885+ expect ( getCommandDecision ( "npm test ${var@P} && echo safe" , allowedCommands , deniedCommands ) ) . toBe (
886+ "auto_deny" ,
887+ )
888+ expect ( getCommandDecision ( "npm install && npm test ${var@P}" , allowedCommands , deniedCommands ) ) . toBe (
889+ "auto_deny" ,
890+ )
891+
892+ // Safe chains should still be auto-approved
893+ expect ( getCommandDecision ( "npm install && echo done" , allowedCommands , deniedCommands ) ) . toBe (
894+ "auto_approve" ,
895+ )
896+ } )
897+
898+ it ( "handles the exact exploit from the security report" , ( ) => {
899+ const exploit = 'echo "${var1=aa\\140whoami\\140c}${var1@P}"'
900+ // Even though 'echo' is in the allowlist, the dangerous pattern prevents auto-approval
901+ expect ( getCommandDecision ( exploit , allowedCommands , deniedCommands ) ) . toBe ( "ask_user" )
902+
903+ // But if it were a denied command, it would still be denied
904+ expect ( getCommandDecision ( `npm test ${ exploit } ` , allowedCommands , deniedCommands ) ) . toBe ( "auto_deny" )
905+ } )
906+ } )
907+
713908 it ( "returns auto_deny for commands with any sub-command auto-denied" , ( ) => {
714909 expect ( getCommandDecision ( "npm test" , allowedCommands , deniedCommands ) ) . toBe ( "auto_deny" )
715910 expect ( getCommandDecision ( "npm install && npm test" , allowedCommands , deniedCommands ) ) . toBe ( "auto_deny" )
0 commit comments