Skip to main content

deepseek_rust_cli/agent/
security.rs

1use 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 operations that modify state
56        "git_commit",
57        "git_push",
58        "git_checkout",
59        "git_clone",
60        "git_stash",
61        // GitHub API write operations
62        "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}