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" {
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 "write_local_file",
49 "replace_text_in_file",
50 "delete_file",
51 "rename_file",
52 "run_python_code",
53 "fetch_url",
54 "get_env_var",
55 "git_commit",
57 "git_push",
58 "git_checkout",
59 "git_clone",
60 "git_stash",
61 "github_issue_create",
63 "github_issue_update",
64 "github_pr_create",
65 "github_pr_merge",
66 ]
67 .iter()
68 .map(|s| s.to_string())
69 .collect()
70}
71
72pub fn is_path_traversal_arg(args: &serde_json::Map<String, Value>) -> bool {
73 let path_keys = [
74 "file_path",
75 "path",
76 "source_path",
77 "destination_path",
78 "output_path",
79 "file1",
80 "file2",
81 "dest",
82 "files",
83 "dir",
84 ];
85 for key in path_keys {
86 if let Some(val) = args.get(key).and_then(|v| v.as_str()) {
87 if is_traversal_path(val) {
88 return true;
89 }
90 for part in val.split([',', ';']) {
91 let part_trimmed = part.trim();
92 if !part_trimmed.is_empty() && is_traversal_path(part_trimmed) {
93 return true;
94 }
95 }
96 }
97 }
98 false
99}
100
101pub fn normalize_path(path: &std::path::Path) -> std::path::PathBuf {
102 use std::path::Component;
103 let mut components = path.components().peekable();
104 let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek() {
105 let buf = std::path::PathBuf::from(c.as_os_str());
106 components.next();
107 buf
108 } else {
109 std::path::PathBuf::new()
110 };
111
112 let mut normalized = Vec::new();
113 for component in components {
114 match component {
115 Component::Prefix(..) => unreachable!(),
116 Component::RootDir => {
117 ret.push(Component::RootDir.as_os_str());
118 }
119 Component::CurDir => {}
120 Component::ParentDir => {
121 if let Some(Component::Normal(_)) = normalized.last() {
122 normalized.pop();
123 } else if ret.as_os_str().is_empty() || ret == std::path::Path::new("/") {
124 normalized.push(Component::ParentDir);
125 }
126 }
127 Component::Normal(c) => {
128 normalized.push(Component::Normal(c));
129 }
130 }
131 }
132 for component in normalized {
133 ret.push(component.as_os_str());
134 }
135 ret
136}
137
138fn canonicalize_any(path: &std::path::Path) -> std::path::PathBuf {
139 match std::fs::canonicalize(path) {
140 Ok(c) => c,
141 Err(_) => {
142 let mut ancestor = path;
143 let mut components = Vec::new();
144 while let Some(parent) = ancestor.parent() {
145 if let Some(file_name) = ancestor.file_name() {
146 components.push(file_name);
147 }
148 if parent.exists() {
149 if let Ok(can_parent) = std::fs::canonicalize(parent) {
150 let mut result = can_parent;
151 for comp in components.iter().rev() {
152 result.push(comp);
153 }
154 return result;
155 }
156 break;
157 }
158 ancestor = parent;
159 }
160 path.to_path_buf()
161 }
162 }
163}
164
165fn is_traversal_path(path_str: &str) -> bool {
166 let p = std::path::PathBuf::from(path_str);
167 let abs = if p.is_absolute() {
168 p
169 } else {
170 match std::env::current_dir() {
171 Ok(mut a) => {
172 a.push(p);
173 a
174 }
175 Err(_) => return false,
176 }
177 };
178 let normalized = normalize_path(&abs);
179 let canonical = canonicalize_any(&normalized);
180
181 let root = crate::tools::base::STARTUP_DIR
182 .get()
183 .cloned()
184 .unwrap_or_else(|| {
185 std::env::current_dir()
186 .and_then(std::fs::canonicalize)
187 .unwrap_or_else(|_| std::path::PathBuf::from("."))
188 });
189 if !canonical.starts_with(&root) && !path_str.is_empty() {
190 return true;
191 }
192 false
193}