1const DANGEROUS_REQUIRES: &[&str] = &[
7 "child_process",
8 "fs",
9 "net",
10 "dgram",
11 "http",
12 "https",
13 "os",
14 "cluster",
15 "vm",
16 "worker_threads",
17];
18
19const DANGEROUS_GLOBALS: &[&str] = &[
20 "eval(",
21 "Function(",
22 "process.exit(",
23 "process.kill(",
24 "process.env",
25 "child_process",
26];
27
28const DANGEROUS_METHODS: &[&str] = &[
29 ".execSync(",
30 ".spawnSync(",
31 ".exec(",
32 ".spawn(",
33 ".fork(",
34 ".writeFileSync(",
35 ".writeFile(",
36 ".unlinkSync(",
37 ".unlink(",
38 ".rmSync(",
39 ".rmdirSync(",
40 ".renameSync(",
41 ".mkdirSync(",
42 ".appendFileSync(",
43 ".createWriteStream(",
44];
45
46#[must_use]
51pub fn is_node_source_safe(source: &str) -> bool {
52 !has_dangerous_requires(source)
53 && !has_dangerous_globals(source)
54 && !has_dangerous_methods(source)
55}
56
57fn has_dangerous_requires(source: &str) -> bool {
58 for module in DANGEROUS_REQUIRES {
59 if source.contains(&format!("require(\"{module}\")"))
61 || source.contains(&format!("require('{module}')"))
62 {
63 return true;
64 }
65 if source.contains(&format!("from \"{module}\""))
67 || source.contains(&format!("from '{module}'"))
68 {
69 return true;
70 }
71 }
72 false
73}
74
75fn has_dangerous_globals(source: &str) -> bool {
76 DANGEROUS_GLOBALS.iter().any(|g| source.contains(g))
77}
78
79fn has_dangerous_methods(source: &str) -> bool {
80 DANGEROUS_METHODS.iter().any(|m| source.contains(m))
81}
82
83#[cfg(test)]
84#[allow(clippy::unwrap_used)]
85mod tests {
86 use super::*;
87
88 #[test]
89 fn console_log_is_safe() {
90 assert!(is_node_source_safe("console.log('hello')"));
91 }
92
93 #[test]
94 fn math_is_safe() {
95 assert!(is_node_source_safe("console.log(Math.sqrt(16))"));
96 }
97
98 #[test]
99 fn json_parse_is_safe() {
100 assert!(is_node_source_safe("JSON.parse('{\"a\":1}')"));
101 }
102
103 #[test]
104 fn empty_source_is_safe() {
105 assert!(is_node_source_safe(""));
106 }
107
108 #[test]
109 fn array_operations_safe() {
110 assert!(is_node_source_safe("[1,2,3].map(x => x * 2)"));
111 }
112
113 #[test]
114 fn require_child_process_is_dangerous() {
115 assert!(!is_node_source_safe(
116 "require('child_process').execSync('ls')"
117 ));
118 }
119
120 #[test]
121 fn require_fs_is_dangerous() {
122 assert!(!is_node_source_safe("require('fs').readFileSync('/')"));
123 }
124
125 #[test]
126 fn require_net_is_dangerous() {
127 assert!(!is_node_source_safe("require('net')"));
128 }
129
130 #[test]
131 fn require_os_is_dangerous() {
132 assert!(!is_node_source_safe("require('os').homedir()"));
133 }
134
135 #[test]
136 fn require_double_quotes_is_dangerous() {
137 assert!(!is_node_source_safe(
138 "require(\"child_process\").exec('ls')"
139 ));
140 }
141
142 #[test]
143 fn import_from_fs_is_dangerous() {
144 assert!(!is_node_source_safe("import { readFileSync } from 'fs'"));
145 }
146
147 #[test]
148 fn eval_is_dangerous() {
149 assert!(!is_node_source_safe("eval('code')"));
150 }
151
152 #[test]
153 fn function_constructor_is_dangerous() {
154 assert!(!is_node_source_safe("new Function('return 1')()"));
155 }
156
157 #[test]
158 fn process_exit_is_dangerous() {
159 assert!(!is_node_source_safe("process.exit(1)"));
160 }
161
162 #[test]
163 fn process_env_is_dangerous() {
164 assert!(!is_node_source_safe("console.log(process.env)"));
165 }
166
167 #[test]
168 fn exec_sync_is_dangerous() {
169 assert!(!is_node_source_safe("cp.execSync('rm -rf /')"));
170 }
171
172 #[test]
173 fn write_file_sync_is_dangerous() {
174 assert!(!is_node_source_safe("fs.writeFileSync('/tmp/x', 'data')"));
175 }
176
177 #[test]
178 fn unlink_sync_is_dangerous() {
179 assert!(!is_node_source_safe("fs.unlinkSync('/tmp/x')"));
180 }
181
182 #[test]
183 fn rm_sync_is_dangerous() {
184 assert!(!is_node_source_safe("fs.rmSync('/', { recursive: true })"));
185 }
186
187 #[test]
188 fn require_vm_is_dangerous() {
189 assert!(!is_node_source_safe("require('vm').runInNewContext('1')"));
190 }
191}