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