Engineering · Jul 5, 2026 · 7 min read

LDAP injection: blind confirmation without enumerating the directory

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.

01The filter string is the attack surface

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.

normal input uid = alice (&(uid=alice)(userPassword=PASS)) matches one entry -- password verified normally injected input uid = alice)(uid=*))(|(a=b (&(uid=alice)(uid=*)) (|(a=b)(userPassword=PASS)) uid=* matches ALL entries -- password check moved outside the AND
String concatenation turns a login field into a filter writer. One closing parenthesis restructures the boolean logic; an asterisk widens the match to the entire directory.

02Confirming with a boolean oracle, not error messages

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.

ldap-probe.pypython
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})")
Proven - CriticalLDAP operator injection - authentication bypass - /api/login
POST /api/login {"username": "admin)(uid=*))(|(a=b", "password": "WRONG-62615533"} ──► 200 OK Set-Cookie: session=... (valid session returned) baseline: admin / WRONG-62615533 ──► 401 Unauthorized differential holds across 5 repeated probes, stable response body
Oracle: differential boolean. No directory entries created or modified. Severity critical - bypass confirmed on an internet-reachable authentication endpoint.

03Server behavior differs - probe per deployment

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.

ServerWildcard handlingConfirming signalNotes
Active DirectoryExpands uid=* against the target OUDifferential: auth response or result-set shape changesError 49 on bind failure; wildcard expansion still returns entries
OpenLDAPProcesses wildcards; LDAP_FILTER_ERROR on unclosed parenthesesDifferential or error code 87 in response bodyVersion-dependent; newer builds apply stricter syntax validation
Apache Directory ServerSchema-validates before execution; strict syntax checkingProtocol error on malformed filter; differential on wildcard-only payloadsError-based detection is reliable here; less so on AD and OpenLDAP
Spring LDAPDepends on LdapTemplate vs. raw string concatenationDifferential; raw concat is the vulnerable code pathLdapTemplate 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.

04Severity follows confirmed capability, not filter structure

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.

5
RFC 4515 metacharacters requiring escape
0
directory entries modified during the probe
2
requests - baseline plus injection - for the full differential

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.

Evidence over heuristicsReachable beats foundMonitor → Block: rolling out a gate without slowing teamsIaC-grounded threat modelingAir-gapped AppSec with no phone-homeOne platform vs three tools
See it on your own app →