Choose your stack

Stacks without a card

How it happens

  • The filter string is built by concatenation or interpolation, so request data is parsed as LDAP filter syntax. cn=(user input) lets the input add operators, clauses and wildcards.
  • A lone * is a presence/substring wildcard: a search meant to find one named person returns every entry the rest of the filter allows, disclosing the whole staff list.
  • A value such as Alice)(mail=a* closes the cn assertion and adds a second, balanced clause. Because it stays a single valid filter, a strict RFC 4515 parser accepts it, and the attacker learns an attribute the feature never returns by asking yes/no questions one character at a time (a blind oracle).
  • Hand-made fixes escape the wrong characters. DN escaping (escape_rdn, LdapEncoder.nameEncode) is for distinguished names and leaves *, ( and ) untouched; a blocklist of only parentheses still lets * through.
  • The search is scoped too loosely, so a widened or injected filter reaches service accounts, disabled accounts or attributes that should never have been in range.

Fix principles

  • Keep the filter template constant and fill only value positions; never build the filter by pasting request data into it.
  • Escape every untrusted value with the library's RFC 4515 filter helper (ldap3.utils.conv.escape_filter_chars, Spring LDAP's LdapEncoder.filterEncode or a LdapQueryBuilder parameter), which encodes * ( ) \ and NUL as data.
  • Use DN escaping (escape_rdn, escape_dn_chars, LdapEncoder.nameEncode) only for the parts of a distinguished name, never for a filter value: the two escape different character sets.
  • Scope the search to what the feature needs (object class, an employeeType or group restriction, a narrow base DN) so a widened filter still cannot reach accounts or subtrees it should not.
  • Return only the attributes the feature needs, so an injected clause cannot be used to read a value the response was never meant to expose.
  • Reject or log a malformed filter server-side and return a generic error; never echo the rejected value.

The scenario

Every card runs the same staff directory. GET /directory?name= searches an LDAP directory with the filter (&(objectClass=person)(employeeType=staff)(cn=<name>)), so it lists only staff whose common name matches, and <name> is the one untrusted part. The directory also holds service and disabled accounts and a mail attribute the search never returns; the exploits try to reach them. The tests run against an in-process directory that parses and evaluates the real filter grammar (ldap3's MOCK_SYNC strategy in Python; an UnboundID in-memory directory server in Java), so an unescaped metacharacter changes which entries match exactly as a live directory would, with no network or server.

How the cards prove it

  • Run each card's tests in three modes: the fixed implementation passes, and the vulnerable and plausible-but-wrong implementations fail. run-all.mjs does this for every card.
  • The exploit tests assert that a lone * matches nobody (no directory listing) and that two blind-oracle probes that differ only in a guessed character return the same result (no attribute leak). Functional tests keep exact names, names containing parentheses or an asterisk, and no-match searches working, and keep non-staff accounts out.
  • Static analysis is uneven. CodeQL models the ldap3 and Spring LDAP search sinks and their filter sanitizers (read, not executed here), so it can flag the vulnerable and wrong implementations; Semgrep CE's nearest rule is file-local and did not fire, and Bandit has no LDAP-injection plugin. Each card's scannerRules records exactly what ran and what it found.

Identifiers

CWE-90 (Improper Neutralization of Special Elements used in an LDAP Query) is the weakness; OWASP Top 10:2025 folds injection into A05. CAPEC-136 (LDAP Injection) is the attack pattern MITRE links to CWE-90. ASVS 5.0.0 1.2.6 requires that the application protect against LDAP injection or implement specific controls to prevent it.

Attack patterns:

References

  1. OWASP LDAP Injection Prevention Cheat Sheet
  2. RFC 4515: LDAP String Representation of Search Filters
  3. CWE-90: Improper Neutralization of Special Elements used in an LDAP Query ('LDAP Injection')
  4. CAPEC-136: LDAP Injection
  5. OWASP ASVS 5.0.0 V1 Encoding and Sanitization (1.2.6, LDAP injection)
  6. PortSwigger issue definition: LDAP injection

Guides on this weakness