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
7/// 强信号:LDAP 过滤器语法。每条都要求**过滤器的形状**,而不只是标点出现。
8///
9/// 前三条的旧形态只认标点序列,于是命中了正常代码里的日常写法:
10/// `\(\s*&` 命中 rustfmt 的多行调用(`f(\n    &buf,\n)`)、`\(\s*\|` 命中多行
11/// 闭包实参(`map(\n    |x| x * 2,\n)`)、`\(!\s*\(` 命中 C/JS 的
12/// `if (!(a == b))` —— 三条都是 `Severity::High`,单条即越过拒绝线。
13/// LDAP 的 `&`/`|` 后面必然跟一个过滤器(即 `(`),`!` 后面必然是一个属性断言,
14/// 按这个形状收紧后误报消失、过滤器照报。
15/// 8 条分支合并成 1 条 alternation —— `regex_detect` 对列表里每条 `Regex` 各跑一次
16/// `find`,干净输入下 8 次全文扫描变 1 次。分支顺序 = 原 vec 顺序(同一位置上取最左
17/// 分支;与「按列表顺序取第一条命中的模式」相比偏移量可能不同,档位不变)。
18static PATTERNS: LazyLock<Regex> = LazyLock::new(|| {
19    Regex::new(concat!(
20        // (&(…
21        r"\(\s*&\s*\(",
22        // (|(…
23        r"|\(\s*\|\s*\(",
24        // (!(attr op value):op 是单个 `=`(`~=` / `>=` / `<=` 也认),
25        // `==` / `!=` 是 C/JS 的相等比较,不是 LDAP。
26        r"|\(!\s*\(\s*[a-zA-Z][\w.-]*\s*[~<>]?=[^=]",
27        r"|\*\(cn=",
28        r"|\(\s*objectClass\s*=",
29        r"|\(\s*uid\s*=",
30        r"|\)\s*\((?:&|\||!)",
31        r"|\(\s*cn\s*=",
32    ))
33    .unwrap()
34});
35
36pub struct LdapInjectionDetector;
37
38impl Detector for LdapInjectionDetector {
39    fn name(&self) -> &'static str {
40        "ldap_injection"
41    }
42
43    fn detect(&self, input: &str) -> Option<DetectionResult> {
44        regex_detect(
45            std::slice::from_ref(&*PATTERNS),
46            self.name(),
47            AttackCategory::Injection,
48            Severity::High,
49            "LDAP injection detected",
50            input,
51        )
52    }
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58
59    fn det() -> LdapInjectionDetector {
60        LdapInjectionDetector
61    }
62
63    fn assert_hit(input: &str) {
64        crate::test_helpers::assert_detected(
65            &det(),
66            input,
67            AttackCategory::Injection,
68            Severity::High,
69        );
70    }
71
72    #[test]
73    fn name_is_ldap_injection() {
74        assert_eq!(det().name(), "ldap_injection");
75    }
76
77    #[test]
78    fn detects_common_payloads() {
79        for input in [
80            "(&(uid=admin)(!(|(cn=*))))",
81            "(&(cn=user))",
82            "(|(cn=admin))",
83            "*(cn=*)",
84            "(!(uid=*))",
85            "(objectClass=*)",
86            ")(&(uid=admin))",
87        ] {
88            assert_hit(input);
89        }
90    }
91
92    #[test]
93    fn benign_inputs_not_detected() {
94        for input in [
95            "Hello, this is a normal text input. Nothing suspicious here.",
96            "Please enter your username and password",
97            "The directory contains user records",
98            "uid=admin",
99            "cn=test",
100        ] {
101            assert!(det().detect(input).is_none(), "false positive: {input}");
102        }
103    }
104
105    #[test]
106    fn edge_cases() {
107        assert!(det().detect("").is_none());
108        assert!(det().detect(" \t\n ").is_none());
109        assert!(det().detect("你好世界 こんにちは").is_none());
110        // near misses: attribute present but not in filter form
111        assert!(det().detect("(uidadmin)").is_none());
112        assert!(det().detect("(xuid=1)").is_none());
113        assert!(det().detect("user (uid) admin").is_none());
114    }
115
116    #[test]
117    fn obfuscated_variants_detected() {
118        for input in ["(&(UID=admin))", "( uid =*)", "( cn = * )"] {
119            assert_hit(input);
120        }
121    }
122
123    /// 反向对照:LDAP 运算符的**标点序列**在正常代码里是日常写法。
124    /// 收紧前这四条全判 High(单条即拒绝)——「`&`/`|` 另起一行」是 rustfmt
125    /// 对长参数列表和多行闭包的默认排版,「`!(…)`」是 C/JS 的取反写法。
126    #[test]
127    fn operators_in_ordinary_code_are_not_detected() {
128        for input in [
129            // rustfmt 的多行调用:`&` 实参另起一行
130            "let n = write_all(\n    &buf,\n    &mut file,\n);\n",
131            // rustfmt 的多行闭包实参:`|` 另起一行
132            "let v = items.iter().map(\n    |x| x * 2,\n).collect::<Vec<_>>();\n",
133            // C / JS 的取反括号表达式(`==` 不是 LDAP 的运算符)
134            "if (!(a == b)) { return EINVAL; }\n",
135            "if (!(a != b) && !(c)) { abort(); }\n",
136        ] {
137            crate::test_helpers::assert_clean(&det(), input);
138        }
139    }
140
141    /// 收紧的另一半:三个运算符家族的过滤器形态必须照报。
142    #[test]
143    fn operator_filters_still_detected_after_tightening() {
144        for input in [
145            "(&(uid=admin)(!(|(cn=*))))",
146            "(|(cn=admin))",
147            "(!(uid=*))",
148            "(!(cn~=adm))",
149        ] {
150            assert_hit(input);
151        }
152    }
153}