@@ -48,6 +48,11 @@ pub struct ExecuteCommand {
4848
4949impl ExecuteCommand {
5050 pub fn requires_acceptance ( & self , allowed_commands : Option < & Vec < String > > , allow_read_only : bool ) -> bool {
51+ // Always require acceptance for multi-line commands.
52+ if self . command . contains ( "\n " ) || self . command . contains ( "\r " ) {
53+ return true ;
54+ }
55+
5156 let default_arr = vec ! [ ] ;
5257 let allowed_commands = allowed_commands. unwrap_or ( & default_arr) ;
5358
@@ -64,7 +69,7 @@ impl ExecuteCommand {
6469 let Some ( args) = shlex:: split ( & self . command ) else {
6570 return true ;
6671 } ;
67- const DANGEROUS_PATTERNS : & [ & str ] = & [ "<(" , "$(" , "`" , ">" , "&&" , "||" , "&" , ";" ] ;
72+ const DANGEROUS_PATTERNS : & [ & str ] = & [ "<(" , "$(" , "`" , ">" , "&&" , "||" , "&" , ";" , "${" , " \n " , " \r " , "IFS" ] ;
6873
6974 if args
7075 . iter ( )
@@ -106,6 +111,7 @@ impl ExecuteCommand {
106111 arg. contains ( "-exec" ) // includes -execdir
107112 || arg. contains ( "-delete" )
108113 || arg. contains ( "-ok" ) // includes -okdir
114+ || arg. contains ( "-fprint" ) // includes -fprint0 and -fprintf
109115 } ) =>
110116 {
111117 return true ;
@@ -114,7 +120,11 @@ impl ExecuteCommand {
114120 // Special casing for `grep`. -P flag for perl regexp has RCE issues, apparently
115121 // should not be supported within grep but is flagged as a possibility since this is perl
116122 // regexp.
117- if cmd == "grep" && cmd_args. iter ( ) . any ( |arg| arg. contains ( "-P" ) ) {
123+ if cmd == "grep"
124+ && cmd_args
125+ . iter ( )
126+ . any ( |arg| arg. contains ( "-P" ) || arg. contains ( "--perl-regexp" ) )
127+ {
118128 return true ;
119129 }
120130 let is_cmd_read_only = READONLY_COMMANDS . contains ( & cmd. as_str ( ) ) ;
@@ -226,7 +236,9 @@ impl ExecuteCommand {
226236 return PermissionEvalResult :: Deny ( denied_match_set) ;
227237 }
228238
229- if !is_in_allowlist || self . requires_acceptance ( Some ( & allowed_commands) , allow_read_only) {
239+ if is_in_allowlist {
240+ PermissionEvalResult :: Allow
241+ } else if self . requires_acceptance ( Some ( & allowed_commands) , allow_read_only) {
230242 PermissionEvalResult :: Ask
231243 } else {
232244 PermissionEvalResult :: Allow
@@ -293,6 +305,14 @@ mod tests {
293305 ( "cat <<< 'some string here' > myimportantfile" , true ) ,
294306 ( "echo '\n #!/usr/bin/env bash\n echo hello\n ' > myscript.sh" , true ) ,
295307 ( "cat <<EOF > myimportantfile\n hello world\n EOF" , true ) ,
308+ // newline checks
309+ ( "which ls\n touch asdf" , true ) ,
310+ ( "which ls\r touch asdf" , true ) ,
311+ // $IFS check
312+ (
313+ r#"IFS=';'; for cmd in "which ls;touch asdf"; do eval "$cmd"; done"# ,
314+ true ,
315+ ) ,
296316 // Safe piped commands
297317 ( "find . -name '*.rs' | grep main" , false ) ,
298318 ( "ls -la | grep .git" , false ) ,
@@ -310,8 +330,12 @@ mod tests {
310330 true ,
311331 ) ,
312332 ( "find important-dir/ -name '*.txt'" , false ) ,
333+ ( r#"find / -fprintf "/path/to/file" <data-to-write> -quit"# , true ) ,
334+ ( r"find . -${t}exec touch asdf \{\} +" , true ) ,
335+ ( r"find . -${t:=exec} touch asdf2 \{\} +" , true ) ,
313336 // `grep` command arguments
314337 ( "echo 'test data' | grep -P '(?{system(\" date\" )})'" , true ) ,
338+ ( "echo 'test data' | grep --perl-regexp '(?{system(\" date\" )})'" , true ) ,
315339 ] ;
316340 for ( cmd, expected) in cmds {
317341 let tool = serde_json:: from_value :: < ExecuteCommand > ( serde_json:: json!( {
@@ -412,6 +436,7 @@ mod tests {
412436 map. insert (
413437 ToolSettingTarget ( tool_name. to_string ( ) ) ,
414438 serde_json:: json!( {
439+ "allowedCommands" : [ "allow_wild_card .*" , "allow_exact" ] ,
415440 "deniedCommands" : [ "git .*" ]
416441 } ) ,
417442 ) ;
@@ -429,13 +454,34 @@ mod tests {
429454 assert ! ( matches!( res, PermissionEvalResult :: Deny ( ref rules) if rules. contains( & "\\ Agit .*\\ z" . to_string( ) ) ) ) ;
430455
431456 let tool_two = serde_json:: from_value :: < ExecuteCommand > ( serde_json:: json!( {
432- "command" : "echo hello " ,
457+ "command" : "this_is_not_a_read_only_command " ,
433458 } ) )
434459 . unwrap ( ) ;
435460
436461 let res = tool_two. eval_perm ( & agent) ;
437462 assert ! ( matches!( res, PermissionEvalResult :: Ask ) ) ;
438463
464+ let tool_allow_wild_card = serde_json:: from_value :: < ExecuteCommand > ( serde_json:: json!( {
465+ "command" : "allow_wild_card some_arg" ,
466+ } ) )
467+ . unwrap ( ) ;
468+ let res = tool_allow_wild_card. eval_perm ( & agent) ;
469+ assert ! ( matches!( res, PermissionEvalResult :: Allow ) ) ;
470+
471+ let tool_allow_exact_should_ask = serde_json:: from_value :: < ExecuteCommand > ( serde_json:: json!( {
472+ "command" : "allow_exact some_arg" ,
473+ } ) )
474+ . unwrap ( ) ;
475+ let res = tool_allow_exact_should_ask. eval_perm ( & agent) ;
476+ assert ! ( matches!( res, PermissionEvalResult :: Ask ) ) ;
477+
478+ let tool_allow_exact_should_allow = serde_json:: from_value :: < ExecuteCommand > ( serde_json:: json!( {
479+ "command" : "allow_exact" ,
480+ } ) )
481+ . unwrap ( ) ;
482+ let res = tool_allow_exact_should_allow. eval_perm ( & agent) ;
483+ assert ! ( matches!( res, PermissionEvalResult :: Allow ) ) ;
484+
439485 agent. allowed_tools . insert ( tool_name. to_string ( ) ) ;
440486
441487 let res = tool_two. eval_perm ( & agent) ;
@@ -444,9 +490,6 @@ mod tests {
444490 // Denied list should remain denied
445491 let res = tool_one. eval_perm ( & agent) ;
446492 assert ! ( matches!( res, PermissionEvalResult :: Deny ( ref rules) if rules. contains( & "\\ Agit .*\\ z" . to_string( ) ) ) ) ;
447-
448- let res = tool_two. eval_perm ( & agent) ;
449- assert ! ( matches!( res, PermissionEvalResult :: Allow ) ) ;
450493 }
451494
452495 #[ tokio:: test]
0 commit comments