deepseek_rust_cli/agent/
security.rs1use std::{collections::HashSet, sync::OnceLock};
2
3use regex::Regex;
4use serde_json::Value;
5
6static DANGEROUS_RE: OnceLock<Vec<Regex>> = OnceLock::new();
7
8pub fn is_dangerous_tool(name: &str, args: &serde_json::Map<String, Value>) -> bool {
9 if name == "delete_file" {
10 return true;
11 }
12 if name == "execute_shell_command" || name == "start_background_process" {
13 let cmd = args
14 .get("command")
15 .and_then(|v| v.as_str())
16 .unwrap_or("")
17 .to_lowercase();
18
19 let regexes = DANGEROUS_RE.get_or_init(|| {
20 let patterns = [
21 r"(\b|[`$])rm(\b|[`$])",
22 r"(\b|[`$])del(\b|[`$])",
23 r"(\b|[`$])rd(\b|[`$])",
24 r"(\b|[`$])rmdir(\b|[`$])",
25 r"(\b|[`$])erase(\b|[`$])",
26 r"(\b|[`$])dd\b.*\bof=",
27 r"(\b|[`$])mkfs(\b|[`$])",
28 r">\s*/dev/",
29 r"(\b|[`$])chown\b.*-R\b",
30 r"(\b|[`$])chmod\b.*777\b",
31 r"(\b|[`$])shred(\b|[`$])",
32 ];
33 patterns.iter().map(|p| Regex::new(p).unwrap()).collect()
34 });
35
36 for re in regexes {
37 if re.is_match(&cmd) {
38 return true;
39 }
40 }
41 }
42 false
43}
44
45pub fn get_approval_required_tools() -> HashSet<String> {
46 [
47 "execute_shell_command",
48 "start_background_process",
49 "write_local_file",
50 "replace_text_in_file",
51 "delete_file",
52 "rename_file",
53 "run_python_code",
54 "fetch_url",
55 "get_env_var",
56 "git_commit",
58 "git_push",
59 "git_checkout",
60 "git_clone",
61 "git_stash",
62 "github_issue_create",
64 "github_issue_update",
65 "github_pr_create",
66 "github_pr_merge",
67 ]
68 .iter()
69 .map(|s| s.to_string())
70 .collect()
71}
72
73pub fn is_path_traversal_arg(args: &serde_json::Map<String, Value>) -> bool {
74 let path_keys = [
75 "file_path",
76 "path",
77 "source_path",
78 "destination_path",
79 "output_path",
80 "file1",
81 "file2",
82 "dest",
83 "files",
84 "dir",
85 ];
86 for key in path_keys {
87 if let Some(val) = args.get(key).and_then(|v| v.as_str()) {
88 if is_traversal_path(val) {
89 return true;
90 }
91 for part in val.split([',', ';']) {
92 let part_trimmed = part.trim();
93 if !part_trimmed.is_empty() && is_traversal_path(part_trimmed) {
94 return true;
95 }
96 }
97 }
98 }
99 false
100}
101
102pub fn normalize_path(path: &std::path::Path) -> std::path::PathBuf {
103 use std::path::Component;
104 let mut components = path.components().peekable();
105 let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek() {
106 let buf = std::path::PathBuf::from(c.as_os_str());
107 components.next();
108 buf
109 } else {
110 std::path::PathBuf::new()
111 };
112
113 let mut normalized = Vec::new();
114 for component in components {
115 match component {
116 Component::Prefix(..) => unreachable!(),
117 Component::RootDir => {
118 ret.push(Component::RootDir.as_os_str());
119 }
120 Component::CurDir => {}
121 Component::ParentDir => {
122 if let Some(Component::Normal(_)) = normalized.last() {
123 normalized.pop();
124 } else if ret.as_os_str().is_empty() || ret == std::path::Path::new("/") {
125 normalized.push(Component::ParentDir);
126 }
127 }
128 Component::Normal(c) => {
129 normalized.push(Component::Normal(c));
130 }
131 }
132 }
133 for component in normalized {
134 ret.push(component.as_os_str());
135 }
136 ret
137}
138
139fn canonicalize_any(path: &std::path::Path) -> std::path::PathBuf {
140 match std::fs::canonicalize(path) {
141 Ok(c) => c,
142 Err(_) => {
143 let mut ancestor = path;
144 let mut components = Vec::new();
145 while let Some(parent) = ancestor.parent() {
146 if let Some(file_name) = ancestor.file_name() {
147 components.push(file_name);
148 }
149 if parent.exists() {
150 if let Ok(can_parent) = std::fs::canonicalize(parent) {
151 let mut result = can_parent;
152 for comp in components.iter().rev() {
153 result.push(comp);
154 }
155 return result;
156 }
157 break;
158 }
159 ancestor = parent;
160 }
161 path.to_path_buf()
162 }
163 }
164}
165
166fn is_traversal_path(path_str: &str) -> bool {
167 let p = std::path::PathBuf::from(path_str);
168 let abs = if p.is_absolute() {
169 p
170 } else {
171 match std::env::current_dir() {
172 Ok(mut a) => {
173 a.push(p);
174 a
175 }
176 Err(_) => return false,
177 }
178 };
179 let normalized = normalize_path(&abs);
180 let canonical = canonicalize_any(&normalized);
181
182 let root = crate::tools::base::STARTUP_DIR
183 .get()
184 .cloned()
185 .unwrap_or_else(|| {
186 std::env::current_dir()
187 .and_then(std::fs::canonicalize)
188 .unwrap_or_else(|_| std::path::PathBuf::from("."))
189 });
190 if !canonical.starts_with(&root) && !path_str.is_empty() {
191 return true;
192 }
193 false
194}