Skip to main content

rippy_cli/
node_safety.rs

1//! Heuristic safety analysis for inline Node.js / JavaScript source code.
2//!
3//! Scans for dangerous `require()` calls, globals, and method calls.
4//! Returns `true` if no dangerous patterns are found.
5
6const 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/// Check whether inline Node.js source appears safe to execute.
47///
48/// This is a heuristic check — it may have false positives (blocking safe code)
49/// but should not have false negatives (allowing dangerous code).
50#[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        // require("module") or require('module')
60        if source.contains(&format!("require(\"{module}\")"))
61            || source.contains(&format!("require('{module}')"))
62        {
63            return true;
64        }
65        // import ... from "module" (ES modules)
66        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}