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
42pub struct UploadDetector;
43
44impl Detector for UploadDetector {
45 fn name(&self) -> &'static str {
46 "upload"
47 }
48
49 fn detect(&self, input: &str) -> Option<DetectionResult> {
50 regex_detect(
51 std::slice::from_ref(&*PATTERNS),
52 self.name(),
53 AttackCategory::File,
54 Severity::Critical,
55 "Malicious file upload detected",
56 input,
57 )
58 }
59}
60
61#[cfg(test)]
62mod tests {
63 use super::*;
64
65 #[test]
66 fn name_returns_attack_type() {
67 assert_eq!(UploadDetector.name(), "upload");
68 }
69
70 #[test]
71 fn detects_php_tags() {
72 for payload in [
73 "<?php system($_GET['cmd']); ?>",
74 "<?= shell_exec($_POST['cmd']) ?>",
75 "<?php echo 'hello';",
76 ] {
77 let r = UploadDetector
78 .detect(payload)
79 .unwrap_or_else(|| panic!("expected detection for {:?}", payload));
80 assert_eq!(r.attack_type, "upload");
81 assert_eq!(r.category, AttackCategory::File);
82 assert_eq!(r.severity, Severity::Critical);
83 assert!(
84 !r.matched_pattern.is_empty(),
85 "matched_pattern empty for {:?}",
86 payload
87 );
88 assert!(
89 r.offset <= payload.len(),
90 "offset out of range for {:?}",
91 payload
92 );
93 }
94 }
95
96 #[test]
97 fn detects_asp_and_script_language_tags() {
98 for payload in [
99 "<% @ Page Language=\"C#\" %>",
100 "<% = response.write(1) %>",
101 "<script language='vbscript'>MsgBox 1</script>",
102 "<script language=\"jscript\">x()</script>",
103 "<script language=php>echo 1;</script>",
104 ] {
105 let r = UploadDetector
106 .detect(payload)
107 .unwrap_or_else(|| panic!("expected detection for {:?}", payload));
108 assert!(
109 !r.matched_pattern.is_empty(),
110 "matched_pattern empty for {:?}",
111 payload
112 );
113 assert!(
114 r.offset <= payload.len(),
115 "offset out of range for {:?}",
116 payload
117 );
118 }
119 }
120
121 #[test]
122 fn detects_exec_functions_and_superglobals() {
123 for payload in [
124 "eval($code);",
125 "system($cmd);",
126 "exec($cmd);",
127 "passthru($cmd);",
128 "shell_exec($cmd);",
129 "$_GET['cmd']",
130 "$_POST['cmd']",
131 "$_REQUEST['cmd']",
132 "$_SERVER['REQUEST_URI']",
133 "base64_decode('aGVsbG8=')",
134 ] {
135 let r = UploadDetector
136 .detect(payload)
137 .unwrap_or_else(|| panic!("expected detection for {:?}", payload));
138 assert!(
139 !r.matched_pattern.is_empty(),
140 "matched_pattern empty for {:?}",
141 payload
142 );
143 assert!(
144 r.offset <= payload.len(),
145 "offset out of range for {:?}",
146 payload
147 );
148 }
149 }
150
151 #[test]
152 fn ignores_benign_inputs() {
153 for input in [
154 "Hello, this is a normal text input.",
155 "php is a popular language",
156 "<script>alert(1)</script>",
157 "system('id')",
158 "exec('ls')",
159 "base64_decode",
160 "$_GET",
161 "eval()",
162 ] {
163 assert!(
164 UploadDetector.detect(input).is_none(),
165 "false positive: {:?}",
166 input
167 );
168 }
169 }
170
171 #[test]
172 fn edge_cases() {
173 assert!(UploadDetector.detect("").is_none());
174 assert!(UploadDetector.detect(" ").is_none());
175 assert!(UploadDetector.detect("<?php echo 1;").is_none()); assert!(UploadDetector.detect("<? phpx echo 1;").is_none()); }
178}