security_rust/data/
deserialization.rs1use crate::{regex_detect, AttackCategory, DetectionResult, Detector, Severity};
4use regex::Regex;
5use std::sync::LazyLock;
6
7static PATTERNS: LazyLock<Vec<Regex>> = LazyLock::new(|| {
8 vec![
9 Regex::new(r#"O:\d+:"#).unwrap(),
10 Regex::new(r#"C:\d+:"#).unwrap(),
11 Regex::new(r"(?i)unserialize\s*\(").unwrap(),
12 Regex::new(r"(?i)__wakeup").unwrap(),
13 Regex::new(r"(?i)__destruct").unwrap(),
14 Regex::new(r"(?i)__construct").unwrap(),
15 Regex::new(r"(?i)__toString").unwrap(),
16 Regex::new(r"(?i)__call").unwrap(),
17 Regex::new(r"(?i)__get").unwrap(),
18 Regex::new(r"(?i)__set").unwrap(),
19 Regex::new(r"a:\d+:\{").unwrap(),
20 ]
21});
22
23pub struct DeserializationDetector;
24
25impl Detector for DeserializationDetector {
26 fn name(&self) -> &'static str {
27 "deserialization"
28 }
29
30 fn detect(&self, input: &str) -> Option<DetectionResult> {
31 regex_detect(&PATTERNS, self.name(), AttackCategory::Data, Severity::Critical, "PHP deserialization attack detected", input)
32 }
33}
34
35#[cfg(test)]
36mod tests {
37 use super::*;
38
39 #[test]
40 fn name_returns_attack_type() {
41 assert_eq!(DeserializationDetector.name(), "deserialization");
42 }
43
44 #[test]
45 fn detects_serialized_php_object() {
46 let input = r#"O:8:"stdClass":1:{s:4:"test";s:5:"value";}"#;
47 let r = DeserializationDetector
48 .detect(input)
49 .expect("serialized PHP object should be detected");
50 assert_eq!(r.attack_type, "deserialization");
51 assert_eq!(r.category, AttackCategory::Data);
52 assert_eq!(r.severity, Severity::Critical);
53 assert_eq!(r.offset, 0);
54 }
55
56 #[test]
57 fn detects_serialized_arrays_and_magic_methods() {
58 for payload in [
59 r#"a:1:{s:4:"key";s:5:"value";}"#,
60 r#"C:5:"Foo":0:{}"#,
61 "unserialize($_POST['data'])",
62 "trigger __wakeup magic method",
63 "call __destruct on shutdown",
64 "override __toString()",
65 ] {
66 let r = DeserializationDetector
67 .detect(payload)
68 .unwrap_or_else(|| panic!("expected detection for {:?}", payload));
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 ignores_benign_inputs() {
84 for input in [
85 "Hello, this is a normal text input.",
86 "Order: 8 items please",
87 "O:8",
88 "unserialize_data is not a call",
89 "constructor and destructor in C++",
90 "a:b:{not a serialized array}",
91 ] {
92 assert!(
93 DeserializationDetector.detect(input).is_none(),
94 "false positive: {:?}",
95 input
96 );
97 }
98 }
99
100 #[test]
101 fn edge_cases() {
102 assert!(DeserializationDetector.detect("").is_none());
103 assert!(DeserializationDetector.detect(" ").is_none());
104 assert!(
105 DeserializationDetector
106 .detect("日本語のテキストです")
107 .is_none()
108 );
109 }
110}