security_rust/file/
upload.rs1use regex::Regex;
4use std::sync::LazyLock;
5
6use crate::{AttackCategory, DetectionResult, Detector, Severity, regex_detect};
7
8static PATTERNS: LazyLock<Regex> = LazyLock::new(|| {
19 Regex::new(
20 &[
21 r"(?i)<\?php",
22 r"|<\?=",
23 r"|<%\s*@",
24 r"|<%\s*=",
25 r#"|<script\s+language\s*=\s*["']?(?:php|vbscript|jscript)["']?"#,
26 r"|eval\s*\(\s*\$",
27 r"|system\s*\(\s*\$",
28 r"|exec\s*\(\s*\$",
29 r"|passthru\s*\(\s*\$",
30 r"|shell_exec\s*\(\s*\$",
31 r"|\$_GET\[",
32 r"|\$_POST\[",
33 r"|\$_REQUEST\[",
34 r"|\$_SERVER\[",
35 r"|base64_decode\s*\(",
36 ]
37 .concat(),
38 )
39 .unwrap()
40});
41
42static B64_NEEDLE: LazyLock<Regex> = LazyLock::new(|| Regex::new("(?i)base64_decode").unwrap());
48
49pub struct UploadDetector;
50
51impl Detector for UploadDetector {
52 fn name(&self) -> &'static str {
53 "upload"
54 }
55
56 fn detect(&self, input: &str) -> Option<DetectionResult> {
57 if !input.contains('<') && !input.contains('$') && !B64_NEEDLE.is_match(input) {
61 return None;
62 }
63 regex_detect(
64 std::slice::from_ref(&*PATTERNS),
65 self.name(),
66 AttackCategory::File,
67 Severity::Critical,
68 "Malicious file upload detected",
69 input,
70 )
71 }
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77
78 #[test]
79 fn name_returns_attack_type() {
80 assert_eq!(UploadDetector.name(), "upload");
81 }
82
83 #[test]
84 fn detects_php_tags() {
85 for payload in [
86 "<?php system($_GET['cmd']); ?>",
87 "<?= shell_exec($_POST['cmd']) ?>",
88 "<?php echo 'hello';",
89 ] {
90 let r = UploadDetector
91 .detect(payload)
92 .unwrap_or_else(|| panic!("expected detection for {:?}", payload));
93 assert_eq!(r.attack_type, "upload");
94 assert_eq!(r.category, AttackCategory::File);
95 assert_eq!(r.severity, Severity::Critical);
96 assert!(
97 !r.matched_pattern.is_empty(),
98 "matched_pattern empty for {:?}",
99 payload
100 );
101 assert!(
102 r.offset <= payload.len(),
103 "offset out of range for {:?}",
104 payload
105 );
106 }
107 }
108
109 #[test]
110 fn detects_asp_and_script_language_tags() {
111 for payload in [
112 "<% @ Page Language=\"C#\" %>",
113 "<% = response.write(1) %>",
114 "<script language='vbscript'>MsgBox 1</script>",
115 "<script language=\"jscript\">x()</script>",
116 "<script language=php>echo 1;</script>",
117 ] {
118 let r = UploadDetector
119 .detect(payload)
120 .unwrap_or_else(|| panic!("expected detection for {:?}", payload));
121 assert!(
122 !r.matched_pattern.is_empty(),
123 "matched_pattern empty for {:?}",
124 payload
125 );
126 assert!(
127 r.offset <= payload.len(),
128 "offset out of range for {:?}",
129 payload
130 );
131 }
132 }
133
134 #[test]
135 fn detects_exec_functions_and_superglobals() {
136 for payload in [
137 "eval($code);",
138 "system($cmd);",
139 "exec($cmd);",
140 "passthru($cmd);",
141 "shell_exec($cmd);",
142 "$_GET['cmd']",
143 "$_POST['cmd']",
144 "$_REQUEST['cmd']",
145 "$_SERVER['REQUEST_URI']",
146 "base64_decode('aGVsbG8=')",
147 ] {
148 let r = UploadDetector
149 .detect(payload)
150 .unwrap_or_else(|| panic!("expected detection for {:?}", payload));
151 assert!(
152 !r.matched_pattern.is_empty(),
153 "matched_pattern empty for {:?}",
154 payload
155 );
156 assert!(
157 r.offset <= payload.len(),
158 "offset out of range for {:?}",
159 payload
160 );
161 }
162 }
163
164 #[test]
165 fn ignores_benign_inputs() {
166 for input in [
167 "Hello, this is a normal text input.",
168 "php is a popular language",
169 "<script>alert(1)</script>",
170 "system('id')",
171 "exec('ls')",
172 "base64_decode",
173 "$_GET",
174 "eval()",
175 ] {
176 assert!(
177 UploadDetector.detect(input).is_none(),
178 "false positive: {:?}",
179 input
180 );
181 }
182 }
183
184 #[test]
187 fn detects_case_insensitive_base64_decode() {
188 for payload in [
189 "BASE64_DECODE('aGVsbG8=')",
190 "BaSe64_DeCoDe('x')",
191 "ba\u{17f}e64_decode('x')", ] {
193 assert!(
194 UploadDetector.detect(payload).is_some(),
195 "missed {:?}",
196 payload
197 );
198 }
199 }
200
201 #[test]
202 fn trigger_free_long_input_stays_clean() {
203 let clean = "The quick brown fox jumps over the lazy dog and then walks back home again "
206 .repeat(2000);
207 assert!(UploadDetector.detect(&clean).is_none());
208 }
209
210 #[test]
211 fn edge_cases() {
212 assert!(UploadDetector.detect("").is_none());
213 assert!(UploadDetector.detect(" ").is_none());
214 assert!(UploadDetector.detect("<?php echo 1;").is_none()); assert!(UploadDetector.detect("<? phpx echo 1;").is_none()); }
217}