bext-waf 0.2.0

Web Application Firewall for bext — rate limiting, IP filtering, GeoIP, rule engine
Documentation
//! Deserialization attack detection.
//!
//! Detects serialized object payloads that can trigger remote code execution
//! when deserialized: Java ObjectInputStream, PHP serialize(), Python pickle,
//! .NET BinaryFormatter, and Ruby Marshal.

use std::sync::OnceLock;

use regex::RegexSet;

static DESER_PATTERNS: OnceLock<RegexSet> = OnceLock::new();

static DESER_DESCRIPTIONS: &[&str] = &[
    "Java serialized object (rO0AB / aced0005)",
    "PHP serialized object (O:N:\"ClassName\")",
    "Python pickle header (\\x80\\x03/\\x80\\x04/\\x80\\x05)",
    ".NET BinaryFormatter (AAEAAAD)",
];

fn patterns() -> &'static RegexSet {
    DESER_PATTERNS.get_or_init(|| {
        RegexSet::new([
            // 0: Java serialized (base64 magic "rO0AB" or hex "aced0005")
            r"(?i)(rO0AB|aced\s*0005)",
            // 1: PHP serialized object: O:4:"User":1:{...} or a:2:{...}
            r#"(?i)[OaCsS]:\d+:"[^"]*":\d+:\{"#,
            // 2: Python pickle v3/v4/v5 header bytes
            r"(?i)(\\x80\\x0[345]|\\x80\\x02)",
            // 3: .NET BinaryFormatter base64 header
            r"(?i)AAEAAAD",
        ])
        .expect("deserialization regex patterns must compile")
    })
}

/// Check an input string for deserialization attack patterns.
pub fn check_deserialization(input: &str) -> Option<String> {
    let set = patterns();
    let matches: Vec<_> = set.matches(input).into_iter().collect();
    if matches.is_empty() {
        None
    } else {
        let idx = matches[0];
        Some(
            DESER_DESCRIPTIONS
                .get(idx)
                .unwrap_or(&"deserialization attack")
                .to_string(),
        )
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn detects_java_serialized() {
        assert!(check_deserialization("rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcA").is_some());
        assert!(check_deserialization("aced0005").is_some());
    }

    #[test]
    fn detects_php_serialized() {
        assert!(check_deserialization(r#"O:4:"User":1:{s:4:"name";s:5:"admin";}"#).is_some());
    }

    #[test]
    fn detects_dotnet_formatter() {
        assert!(check_deserialization("AAEAAAD/////AQAAAA").is_some());
    }

    #[test]
    fn allows_normal_text() {
        assert!(check_deserialization("Hello world").is_none());
        assert!(check_deserialization(r#"{"name":"John"}"#).is_none());
    }
}