security_rust/data/
deserialization.rs1use crate::{AttackCategory, DetectionResult, Detector, Severity, regex_detect};
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(
32 &PATTERNS,
33 self.name(),
34 AttackCategory::Data,
35 Severity::Critical,
36 "PHP deserialization attack detected",
37 input,
38 )
39 }
40}
41
42#[cfg(test)]
43mod tests {
44 use super::*;
45
46 #[test]
47 fn name_returns_attack_type() {
48 assert_eq!(DeserializationDetector.name(), "deserialization");
49 }
50
51 #[test]
52 fn detects_serialized_php_object() {
53 let input = r#"O:8:"stdClass":1:{s:4:"test";s:5:"value";}"#;
54 let r = DeserializationDetector
55 .detect(input)
56 .expect("serialized PHP object should be detected");
57 assert_eq!(r.attack_type, "deserialization");
58 assert_eq!(r.category, AttackCategory::Data);
59 assert_eq!(r.severity, Severity::Critical);
60 assert_eq!(r.offset, 0);
61 }
62
63 #[test]
64 fn detects_serialized_arrays_and_magic_methods() {
65 for payload in [
66 r#"a:1:{s:4:"key";s:5:"value";}"#,
67 r#"C:5:"Foo":0:{}"#,
68 "unserialize($_POST['data'])",
69 "trigger __wakeup magic method",
70 "call __destruct on shutdown",
71 "override __toString()",
72 ] {
73 let r = DeserializationDetector
74 .detect(payload)
75 .unwrap_or_else(|| panic!("expected detection for {:?}", payload));
76 assert!(
77 !r.matched_pattern.is_empty(),
78 "matched_pattern empty for {:?}",
79 payload
80 );
81 assert!(
82 r.offset <= payload.len(),
83 "offset out of range for {:?}",
84 payload
85 );
86 }
87 }
88
89 #[test]
90 fn ignores_benign_inputs() {
91 for input in [
92 "Hello, this is a normal text input.",
93 "Order: 8 items please",
94 "O:8",
95 "unserialize_data is not a call",
96 "constructor and destructor in C++",
97 "a:b:{not a serialized array}",
98 ] {
99 assert!(
100 DeserializationDetector.detect(input).is_none(),
101 "false positive: {:?}",
102 input
103 );
104 }
105 }
106
107 #[test]
108 fn edge_cases() {
109 assert!(DeserializationDetector.detect("").is_none());
110 assert!(DeserializationDetector.detect(" ").is_none());
111 assert!(
112 DeserializationDetector
113 .detect("日本語のテキストです")
114 .is_none()
115 );
116 }
117}