security_rust/injection/
ldap_injection.rs1use crate::{AttackCategory, DetectionResult, Detector, Severity, regex_detect};
4use regex::Regex;
5use std::sync::LazyLock;
6
7static PATTERNS: LazyLock<Regex> = LazyLock::new(|| {
19 Regex::new(concat!(
20 r"\(\s*&\s*\(",
22 r"|\(\s*\|\s*\(",
24 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 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 #[test]
127 fn operators_in_ordinary_code_are_not_detected() {
128 for input in [
129 "let n = write_all(\n &buf,\n &mut file,\n);\n",
131 "let v = items.iter().map(\n |x| x * 2,\n).collect::<Vec<_>>();\n",
133 "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 #[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}