Skip to main content

security_rust/data/
deserialization.rs

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