Skip to main content

security_rust/injection/
ldap_injection.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"\(\s*&").unwrap(),
10        Regex::new(r"\(\s*\|").unwrap(),
11        Regex::new(r"\(!\s*\(").unwrap(),
12        Regex::new(r"\*\(cn=").unwrap(),
13        Regex::new(r"\(\s*objectClass\s*=").unwrap(),
14        Regex::new(r"\(\s*uid\s*=").unwrap(),
15        Regex::new(r"\)\s*\((?:&|\||!)").unwrap(),
16        Regex::new(r"\(\s*cn\s*=").unwrap(),
17    ]
18});
19
20pub struct LdapInjectionDetector;
21
22impl Detector for LdapInjectionDetector {
23    fn name(&self) -> &'static str {
24        "ldap_injection"
25    }
26
27    fn detect(&self, input: &str) -> Option<DetectionResult> {
28        regex_detect(&PATTERNS, self.name(), AttackCategory::Injection, Severity::High, "LDAP injection detected", input)
29    }
30}
31
32#[cfg(test)]
33mod tests {
34    use super::*;
35
36    fn det() -> LdapInjectionDetector {
37        LdapInjectionDetector
38    }
39
40    fn assert_hit(input: &str) {
41        crate::test_helpers::assert_detected(
42            &det(),
43            input,
44            AttackCategory::Injection,
45            Severity::High,
46        );
47    }
48
49    #[test]
50    fn name_is_ldap_injection() {
51        assert_eq!(det().name(), "ldap_injection");
52    }
53
54    #[test]
55    fn detects_common_payloads() {
56        for input in [
57            "(&(uid=admin)(!(|(cn=*))))",
58            "(&(cn=user))",
59            "(|(cn=admin))",
60            "*(cn=*)",
61            "(!(uid=*))",
62            "(objectClass=*)",
63            ")(&(uid=admin))",
64        ] {
65            assert_hit(input);
66        }
67    }
68
69    #[test]
70    fn benign_inputs_not_detected() {
71        for input in [
72            "Hello, this is a normal text input. Nothing suspicious here.",
73            "Please enter your username and password",
74            "The directory contains user records",
75            "uid=admin",
76            "cn=test",
77        ] {
78            assert!(det().detect(input).is_none(), "false positive: {input}");
79        }
80    }
81
82    #[test]
83    fn edge_cases() {
84        assert!(det().detect("").is_none());
85        assert!(det().detect(" \t\n ").is_none());
86        assert!(det().detect("你好世界 こんにちは").is_none());
87        // near misses: attribute present but not in filter form
88        assert!(det().detect("(uidadmin)").is_none());
89        assert!(det().detect("(xuid=1)").is_none());
90        assert!(det().detect("user (uid) admin").is_none());
91    }
92
93    #[test]
94    fn obfuscated_variants_detected() {
95        for input in ["(&(UID=admin))", "( uid =*)", "( cn = * )"] {
96            assert_hit(input);
97        }
98    }
99}