Skip to main content

security_rust/data/
deserialization.rs

1// Copyright (c) 2026 erik <erik@erik.xyz> — https://erik.xyz
2
3use 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}