security_rust/file/
upload.rs1use regex::Regex;
4use std::sync::LazyLock;
5
6use crate::{AttackCategory, DetectionResult, Detector, Severity, regex_detect};
7
8static PATTERNS: LazyLock<Vec<Regex>> = LazyLock::new(|| {
9 vec![
10 Regex::new(r"(?i)<\?php").unwrap(),
11 Regex::new(r"(?i)<\?=").unwrap(),
12 Regex::new(r"(?i)<%\s*@").unwrap(),
13 Regex::new(r"(?i)<%\s*=").unwrap(),
14 Regex::new(r#"(?i)<script\s+language\s*=\s*["']?(?:php|vbscript|jscript)["']?"#).unwrap(),
15 Regex::new(r"(?i)eval\s*\(\s*\$").unwrap(),
16 Regex::new(r"(?i)system\s*\(\s*\$").unwrap(),
17 Regex::new(r"(?i)exec\s*\(\s*\$").unwrap(),
18 Regex::new(r"(?i)passthru\s*\(\s*\$").unwrap(),
19 Regex::new(r"(?i)shell_exec\s*\(\s*\$").unwrap(),
20 Regex::new(r"(?i)\$_GET\[").unwrap(),
21 Regex::new(r"(?i)\$_POST\[").unwrap(),
22 Regex::new(r"(?i)\$_REQUEST\[").unwrap(),
23 Regex::new(r"(?i)\$_SERVER\[").unwrap(),
24 Regex::new(r"(?i)base64_decode\s*\(").unwrap(),
25 ]
26});
27
28pub struct UploadDetector;
29
30impl Detector for UploadDetector {
31 fn name(&self) -> &'static str {
32 "upload"
33 }
34
35 fn detect(&self, input: &str) -> Option<DetectionResult> {
36 regex_detect(
37 &PATTERNS,
38 self.name(),
39 AttackCategory::File,
40 Severity::Critical,
41 "Malicious file upload detected",
42 input,
43 )
44 }
45}
46
47#[cfg(test)]
48mod tests {
49 use super::*;
50
51 #[test]
52 fn name_returns_attack_type() {
53 assert_eq!(UploadDetector.name(), "upload");
54 }
55
56 #[test]
57 fn detects_php_tags() {
58 for payload in [
59 "<?php system($_GET['cmd']); ?>",
60 "<?= shell_exec($_POST['cmd']) ?>",
61 "<?php echo 'hello';",
62 ] {
63 let r = UploadDetector
64 .detect(payload)
65 .unwrap_or_else(|| panic!("expected detection for {:?}", payload));
66 assert_eq!(r.attack_type, "upload");
67 assert_eq!(r.category, AttackCategory::File);
68 assert_eq!(r.severity, Severity::Critical);
69 assert!(
70 !r.matched_pattern.is_empty(),
71 "matched_pattern empty for {:?}",
72 payload
73 );
74 assert!(
75 r.offset <= payload.len(),
76 "offset out of range for {:?}",
77 payload
78 );
79 }
80 }
81
82 #[test]
83 fn detects_asp_and_script_language_tags() {
84 for payload in [
85 "<% @ Page Language=\"C#\" %>",
86 "<% = response.write(1) %>",
87 "<script language='vbscript'>MsgBox 1</script>",
88 "<script language=\"jscript\">x()</script>",
89 "<script language=php>echo 1;</script>",
90 ] {
91 let r = UploadDetector
92 .detect(payload)
93 .unwrap_or_else(|| panic!("expected detection for {:?}", payload));
94 assert!(
95 !r.matched_pattern.is_empty(),
96 "matched_pattern empty for {:?}",
97 payload
98 );
99 assert!(
100 r.offset <= payload.len(),
101 "offset out of range for {:?}",
102 payload
103 );
104 }
105 }
106
107 #[test]
108 fn detects_exec_functions_and_superglobals() {
109 for payload in [
110 "eval($code);",
111 "system($cmd);",
112 "exec($cmd);",
113 "passthru($cmd);",
114 "shell_exec($cmd);",
115 "$_GET['cmd']",
116 "$_POST['cmd']",
117 "$_REQUEST['cmd']",
118 "$_SERVER['REQUEST_URI']",
119 "base64_decode('aGVsbG8=')",
120 ] {
121 let r = UploadDetector
122 .detect(payload)
123 .unwrap_or_else(|| panic!("expected detection for {:?}", payload));
124 assert!(
125 !r.matched_pattern.is_empty(),
126 "matched_pattern empty for {:?}",
127 payload
128 );
129 assert!(
130 r.offset <= payload.len(),
131 "offset out of range for {:?}",
132 payload
133 );
134 }
135 }
136
137 #[test]
138 fn ignores_benign_inputs() {
139 for input in [
140 "Hello, this is a normal text input.",
141 "php is a popular language",
142 "<script>alert(1)</script>",
143 "system('id')",
144 "exec('ls')",
145 "base64_decode",
146 "$_GET",
147 "eval()",
148 ] {
149 assert!(
150 UploadDetector.detect(input).is_none(),
151 "false positive: {:?}",
152 input
153 );
154 }
155 }
156
157 #[test]
158 fn edge_cases() {
159 assert!(UploadDetector.detect("").is_none());
160 assert!(UploadDetector.detect(" ").is_none());
161 assert!(UploadDetector.detect("<?php echo 1;").is_none()); assert!(UploadDetector.detect("<? phpx echo 1;").is_none()); }
164}