An LDAP filter that passes unsanitized input lets an attacker flip authentication logic - confirming it uses differential boolean oracles, not a directory dump.
LDAP injection rewrites the filter an application builds around user input. The canonical auth bypass - injecting a wildcard or a boolean short-circuit into the uid field - requires nothing more than parentheses and an asterisk. What makes it harder to confirm than SQL injection is that well-configured LDAP servers suppress error messages in production, leaving error-signature detection blind. The reliable path is a differential boolean oracle: send one probe that should fail, send one that rewrites the filter to always match, and compare the responses.
Most LDAP authentication builds a query by concatenating user input directly into a filter string. The application assembles (&(uid=USER)(userPassword=PASS)) from the login form, passes that string to an LDAP library, and expects a bind or search result. Any character the server treats as a metacharacter - (, ), *, \, or a null byte - that reaches the string unescaped can restructure the boolean logic. An asterisk alone turns uid=USER into uid=*, matching every entry in the directory. Parentheses go further: closing the uid clause early and appending a new condition can discard the password check entirely and replace it with one that always evaluates to true.
A production LDAP server rarely prints its filter in an error response. Error-based detection that relies on seeing the injected syntax echoed back fails silently against any server with a generic error page. The differential approach requires no error output at all: two probes, one baseline and one injection, with the response difference as the artifact.
The baseline probe sends a known-existing username with a deliberately wrong password - the expected 401 or invalid-credentials response establishes the normal failure state. The injection probe substitutes a payload that, if interpreted as LDAP filter syntax, rewrites the search to match regardless of the password. If the injection probe returns a 200 or a session cookie where the baseline returned 401, the filter was rewritten. The confirming artifact is the response difference - not a string found in an error body - and no directory entry is created, modified, or deleted during either probe.
import requests TARGET = "https://target.example/api/login" NONCE = "62615533" # benign marker, not real credentials def probe(username, password): r = requests.post( TARGET, json={"username": username, "password": password}, timeout=10, ) return r.status_code, "Set-Cookie" in r.headers # baseline: valid username, provably wrong password base_code, base_cookie = probe("admin", "WRONG-" + NONCE) # injection: wildcard rewrites the filter to match any entry inj_code, inj_cookie = probe("admin)(uid=*))(|(a=b", "WRONG-" + NONCE) if base_code == 401 and inj_cookie: print("CONFIRMED: filter rewritten - auth bypass demonstrated") elif base_code == inj_code: print(f"REACHABLE - no differential yet ({base_code})")
Active Directory, OpenLDAP and Apache Directory Server each handle malformed filter syntax differently, and the confirming signal varies across all three. AD expands uid=* against the relevant OU and returns a result set when one matches, while returning LDAP error 49 (invalidCredentials) on a clean bind failure - the differential is the response shape, not an error code. OpenLDAP can silently process wildcard expansion for some filter shapes while returning LDAP_FILTER_ERROR (code 87) for others, depending on version and configuration; the version that returns errors is not safer, it just makes the confirmation more visible. Apache DS validates filter syntax before execution and may reject the request at the protocol layer, making error-based detection possible there but not on the other two. A scanner that pattern-matches one server's error output is a false-negative factory for every other deployment.
| Server | Wildcard handling | Confirming signal | Notes |
|---|---|---|---|
| Active Directory | Expands uid=* against the target OU | Differential: auth response or result-set shape changes | Error 49 on bind failure; wildcard expansion still returns entries |
| OpenLDAP | Processes wildcards; LDAP_FILTER_ERROR on unclosed parentheses | Differential or error code 87 in response body | Version-dependent; newer builds apply stricter syntax validation |
| Apache Directory Server | Schema-validates before execution; strict syntax checking | Protocol error on malformed filter; differential on wildcard-only payloads | Error-based detection is reliable here; less so on AD and OpenLDAP |
| Spring LDAP | Depends on LdapTemplate vs. raw string concatenation | Differential; raw concat is the vulnerable code path | LdapTemplate escapes by default; custom query builders do not |
The implication is per-fingerprint probing. Detecting the LDAP library or server from response headers and error shapes before selecting the injection probe avoids the false negative a single-pattern check creates for every deployment it did not model. The confirming oracle - differential response, not error text - works across all four regardless of what the server says about the malformed filter.
An injection that returns a session for any username without a valid password is an authentication bypass, rated critical regardless of directory size or scope. The impact is the same whether ten or ten million accounts exist: an attacker authenticates as any user without credentials. An injection that only widens a search result set - returning more entries than expected from a directory query form - is rated high until a cross-tenant boundary crossing is confirmed. Attribute injection, where user-supplied input reaches an attribute name in the filter rather than a value, is a different class: it may allow enumeration of schema attributes or access to fields outside the intended query, rated by what the accessible data reveals.
The fix is LDAP-specific escaping at the input boundary. RFC 4515 defines escape sequences for every reserved character: \28 for (, \29 for ), \2a for *, \5c for \, \00 for null. Every major LDAP library ships a function that applies these - Spring LDAP's LdapEncoder.filterEncode(), Python's ldap3.utils.conv.escape_filter_chars(), Java's DirContextOperations.addAttributeValue() within a template. Using the library's escape function is always safer than writing a custom exclusion filter, because exclusion filters miss encoding variants and Unicode normalization paths that bypass them.
The principle is the same as every other injection class in this engine: the confirming artifact is a measurable difference in the response, not a pattern in an error message. A server that suppresses error output is not safe - it is just harder to confirm with a naive signature scanner. A boolean oracle works regardless of whether the server names the vulnerable filter in its response, which is exactly why it is the right instrument here.