Skip to main content

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