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