Skip to content

Commit 5ea7f6a

Browse files
authored
fix: block dangerous patterns for tool calls (#3313)
* first commit * disallowed dangerous patterns as a first priority for executing tools * recovered hooks help * got rid of removeme * got rid of space * refactored requires_acceptance to ensure correct ordering of safeguards and allowed commands * formatted for cargo again
1 parent 51d0245 commit 5ea7f6a

File tree

1 file changed

+36
-19
lines changed
  • crates/chat-cli/src/cli/chat/tools/execute

1 file changed

+36
-19
lines changed

crates/chat-cli/src/cli/chat/tools/execute/mod.rs

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -55,17 +55,6 @@ impl ExecuteCommand {
5555
}
5656

5757
let default_arr = vec![];
58-
let allowed_commands = allowed_commands.unwrap_or(&default_arr);
59-
60-
let has_regex_match = allowed_commands
61-
.iter()
62-
.map(|cmd| Regex::new(&format!(r"\A{}\z", cmd)))
63-
.filter(Result::is_ok)
64-
.flatten()
65-
.any(|regex| regex.is_match(&self.command));
66-
if has_regex_match {
67-
return false;
68-
}
6958

7059
let Some(args) = shlex::split(&self.command) else {
7160
return true;
@@ -102,7 +91,7 @@ impl ExecuteCommand {
10291
}
10392

10493
// Check if each command in the pipe chain starts with a safe command
105-
for cmd_args in all_commands {
94+
for cmd_args in &all_commands {
10695
match cmd_args.first() {
10796
// Special casing for `find` so that we support most cases while safeguarding
10897
// against unwanted mutations
@@ -129,12 +118,29 @@ impl ExecuteCommand {
129118
{
130119
return true;
131120
}
132-
let is_cmd_read_only = READONLY_COMMANDS.contains(&cmd.as_str());
133-
if !allow_read_only || !is_cmd_read_only {
134-
return true;
135-
}
136121
},
137-
None => return true,
122+
None => {},
123+
}
124+
}
125+
126+
let allowed_commands = allowed_commands.unwrap_or(&default_arr);
127+
128+
let has_regex_match = allowed_commands
129+
.iter()
130+
.map(|cmd| Regex::new(&format!(r"\A{}\z", cmd)))
131+
.filter(Result::is_ok)
132+
.flatten()
133+
.any(|regex| regex.is_match(&self.command));
134+
if has_regex_match {
135+
return false;
136+
}
137+
138+
for cmd_args in all_commands {
139+
if let Some(cmd) = cmd_args.first() {
140+
let is_cmd_read_only = READONLY_COMMANDS.contains(&cmd.as_str());
141+
if !allow_read_only || !is_cmd_read_only {
142+
return true;
143+
}
138144
}
139145
}
140146

@@ -423,8 +429,19 @@ mod tests {
423429
("command subcommand a=0123456789 b=0123456789", false),
424430
("command subcommand a=0123456789 b=012345678", true),
425431
("command subcommand alternate a=0123456789 b=0123456789", true),
426-
// Control characters ignored due to direct allowed_command_regex match
427-
("command subcommand && command subcommand", false),
432+
// dangerous patterns
433+
("echo 'test<(data'", true),
434+
("echo 'test$(data)'", true),
435+
("echo 'test`data`'", true),
436+
("echo 'test' > output.txt", true),
437+
("echo 'test data' && touch main.py", true),
438+
("echo 'test' || rm file", true),
439+
("echo 'test' & background", true),
440+
("echo 'test data'; touch main.py", true),
441+
("echo $HOME", true),
442+
("echo 'test\nrm file'", true),
443+
("echo 'test\rrm file'", true),
444+
("IFS=/ malicious", true),
428445
];
429446
for (cmd, expected) in cmds {
430447
let tool = serde_json::from_value::<ExecuteCommand>(serde_json::json!({

0 commit comments

Comments
 (0)