rippy_cli/
python_safety.rs1const DANGEROUS_IMPORTS: &[&str] = &[
7 "os",
8 "subprocess",
9 "socket",
10 "pickle",
11 "shutil",
12 "ctypes",
13 "signal",
14 "pty",
15 "commands",
16 "webbrowser",
17 "tempfile",
18 "pathlib",
19 "io",
20 "multiprocessing",
21 "threading",
22];
23
24const DANGEROUS_BUILTINS: &[&str] = &[
25 "eval(",
26 "exec(",
27 "open(",
28 "__import__(",
29 "compile(",
30 "globals(",
31 "locals(",
32 "getattr(",
33 "setattr(",
34 "delattr(",
35 "breakpoint(",
36];
37
38const DANGEROUS_ATTRIBUTES: &[&str] = &[
39 ".system(",
40 ".popen(",
41 ".call(",
42 ".run(",
43 ".check_output(",
44 ".check_call(",
45 ".Popen(",
46 ".connect(",
47 ".write(",
48 ".remove(",
49 ".rmdir(",
50 ".unlink(",
51 ".rename(",
52 ".mkdir(",
53];
54
55#[must_use]
60pub fn is_python_source_safe(source: &str) -> bool {
61 !has_dangerous_imports(source)
62 && !has_dangerous_builtins(source)
63 && !has_dangerous_attributes(source)
64}
65
66fn has_dangerous_imports(source: &str) -> bool {
67 for token in source.split([';', '\n']) {
68 let trimmed = token.trim();
69 if let Some(rest) = trimmed.strip_prefix("import ") {
71 for module in rest.split(',') {
72 let name = module.split_whitespace().next().unwrap_or("");
73 let top_level = name.split('.').next().unwrap_or("");
75 if DANGEROUS_IMPORTS.contains(&top_level) {
76 return true;
77 }
78 }
79 }
80 if let Some(rest) = trimmed.strip_prefix("from ")
82 && let Some(module_part) = rest.split_whitespace().next()
83 {
84 let top_level = module_part.split('.').next().unwrap_or("");
85 if DANGEROUS_IMPORTS.contains(&top_level) {
86 return true;
87 }
88 }
89 }
90 false
91}
92
93fn has_dangerous_builtins(source: &str) -> bool {
94 DANGEROUS_BUILTINS.iter().any(|b| source.contains(b))
95}
96
97fn has_dangerous_attributes(source: &str) -> bool {
98 DANGEROUS_ATTRIBUTES.iter().any(|a| source.contains(a))
99}
100
101#[cfg(test)]
102#[allow(clippy::unwrap_used)]
103mod tests {
104 use super::*;
105
106 #[test]
107 fn print_is_safe() {
108 assert!(is_python_source_safe("print(1)"));
109 }
110
111 #[test]
112 fn simple_math_is_safe() {
113 assert!(is_python_source_safe("x = 1 + 2; print(x)"));
114 }
115
116 #[test]
117 fn json_import_is_safe() {
118 assert!(is_python_source_safe("import json; print(json.dumps({}))"));
119 }
120
121 #[test]
122 fn sys_import_is_safe() {
123 assert!(is_python_source_safe("import sys; print(sys.version)"));
124 }
125
126 #[test]
127 fn os_import_is_dangerous() {
128 assert!(!is_python_source_safe("import os; os.system('rm -rf /')"));
129 }
130
131 #[test]
132 fn subprocess_import_is_dangerous() {
133 assert!(!is_python_source_safe("import subprocess"));
134 }
135
136 #[test]
137 fn from_os_import_is_dangerous() {
138 assert!(!is_python_source_safe("from os import system"));
139 }
140
141 #[test]
142 fn from_os_path_import_is_dangerous() {
143 assert!(!is_python_source_safe("from os.path import join"));
144 }
145
146 #[test]
147 fn import_os_path_is_dangerous() {
148 assert!(!is_python_source_safe("import os.path"));
149 }
150
151 #[test]
152 fn eval_is_dangerous() {
153 assert!(!is_python_source_safe("eval('code')"));
154 }
155
156 #[test]
157 fn exec_is_dangerous() {
158 assert!(!is_python_source_safe("exec('code')"));
159 }
160
161 #[test]
162 fn open_is_dangerous() {
163 assert!(!is_python_source_safe("open('file')"));
164 }
165
166 #[test]
167 fn dunder_import_is_dangerous() {
168 assert!(!is_python_source_safe("__import__('os')"));
169 }
170
171 #[test]
172 fn compile_is_dangerous() {
173 assert!(!is_python_source_safe("compile('code', '', 'exec')"));
174 }
175
176 #[test]
177 fn attribute_system_is_dangerous() {
178 assert!(!is_python_source_safe("foo.system('cmd')"));
179 }
180
181 #[test]
182 fn attribute_popen_is_dangerous() {
183 assert!(!is_python_source_safe("foo.popen('cmd')"));
184 }
185
186 #[test]
187 fn attribute_connect_is_dangerous() {
188 assert!(!is_python_source_safe("s.connect(('host', 80))"));
189 }
190
191 #[test]
192 fn pickle_import_is_dangerous() {
193 assert!(!is_python_source_safe("import pickle"));
194 }
195
196 #[test]
197 fn socket_import_is_dangerous() {
198 assert!(!is_python_source_safe("import socket"));
199 }
200
201 #[test]
202 fn multiple_safe_imports() {
203 assert!(is_python_source_safe(
204 "import json, math, re; print(json.dumps({'a': math.pi}))"
205 ));
206 }
207
208 #[test]
209 fn mixed_safe_and_dangerous_imports() {
210 assert!(!is_python_source_safe("import json, os"));
211 }
212
213 #[test]
214 fn empty_source_is_safe() {
215 assert!(is_python_source_safe(""));
216 }
217
218 #[test]
219 fn string_operations_safe() {
220 assert!(is_python_source_safe(
221 "s = 'hello'; print(s.upper(), len(s))"
222 ));
223 }
224
225 #[test]
226 fn list_comprehension_safe() {
227 assert!(is_python_source_safe("print([x**2 for x in range(10)])"));
228 }
229
230 #[test]
231 fn breakpoint_is_dangerous() {
232 assert!(!is_python_source_safe("breakpoint()"));
233 }
234
235 #[test]
236 fn getattr_is_dangerous() {
237 assert!(!is_python_source_safe("getattr(obj, 'method')"));
238 }
239
240 #[test]
241 fn multiprocessing_is_dangerous() {
242 assert!(!is_python_source_safe("import multiprocessing"));
243 }
244}