Safeguard
Vulnerability Analysis

LDAP injection vulnerabilities explained

LDAP injection lets attackers manipulate directory filters to bypass authentication or dump data. Here's how CWE-90 attacks work and how to stop them.

Nayan Dey
Security Researcher
6 min read

An LDAP injection vulnerability lets an attacker manipulate the filter syntax of a Lightweight Directory Access Protocol query so the directory server returns, authenticates, or discloses data it never should. LDAP backs authentication and lookup for Active Directory, OpenLDAP, and most enterprise single sign-on stacks, so a single unescaped input field in a login form or search box can translate directly into an authentication bypass or a full directory dump. The bug class is tracked as CWE-90 ("Improper Neutralization of Special Elements used in an LDAP Query") and has sat in OWASP's Injection category (A03:2021) for over a decade, yet it keeps showing up in pentest reports because developers concatenate strings into filters the same way they used to concatenate strings into SQL. Apache Syncope's CVE-2020-1953 is a documented example: a crafted filter value let attackers bypass authentication entirely. Below we break down how the attack works, how it differs from SQL injection, and what actually stops it.

What is an LDAP injection vulnerability?

An LDAP injection vulnerability is a flaw where untrusted input is inserted into an LDAP search filter without proper escaping, letting an attacker change the filter's logic. LDAP filters use a prefix notation with special characters — (, ), *, \, and NUL — that carry structural meaning. A typical login check might build a filter like (&(uid=$username)(userPassword=$password)). If $username is inserted raw and an attacker submits *)(uid=*))(|(uid=*, the resulting filter can evaluate to true for any record, defeating the password check without ever guessing a credential. This is functionally identical to the classic ' OR '1'='1 SQL injection pattern, just expressed in LDAP's filter grammar instead of SQL's. MITRE assigned this class CWE-90 back in 2006, and it remains distinct from CWE-89 (SQL injection) in vulnerability databases specifically because directory servers parse and reject malformed input differently than RDBMSes do.

How does an LDAP injection attack work in practice?

An LDAP injection attack works by injecting filter metacharacters into a field that gets concatenated into a search or bind filter, then observing how the directory server's response changes. There are two practical variants. In a classic (in-band) attack, the attacker injects operators like | (OR) or & (AND) directly into an authentication filter to force a match — the *)(uid=*))(|(uid=* payload above is a textbook example, and it can also be used against attribute filters like (cn=$search) to enumerate every entry in a directory by submitting *. In a blind LDAP injection attack, the application doesn't echo query results, so the attacker infers directory contents one boolean at a time by submitting filters such as (uid=admin)(userPassword=a*) and watching for a true/false difference in the HTTP response (page length, redirect target, or error message), then iterating through characters until a password hash or attribute value is fully extracted. Both variants typically target the bind (authentication) filter or a user/group search filter behind an "employee lookup" or "reset my password" feature — the two most common places raw user input reaches an LDAP query.

What's the difference between LDAP injection and SQL injection?

The core difference is the query language and error surface, not the underlying root cause — both are CWE-707 improper-neutralization bugs where untrusted input crosses into a structured query. SQL injection (CWE-89) targets relational databases and benefits from decades of tooling: parameterized queries, ORMs, and WAF signatures tuned to UNION SELECT and comment-terminator patterns. LDAP injection (CWE-90) targets directory servers that use RFC 4515 filter syntax, which most WAF rulesets and static analyzers cover far less thoroughly than SQL grammar. LDAP servers also tend to fail more quietly than databases — a malformed filter often just returns zero results instead of throwing a verbose stack trace, which is exactly why blind LDAP injection is so common in the wild relative to blind SQL injection. Additionally, LDAP has no native equivalent to SQL's prepared-statement placeholder that every driver enforces by default; escaping is left to the application layer (Java's javax.naming, PHP's ldap_escape() added in PHP 5.6/7.0, or manual character replacement), so the safe path is opt-in rather than the default.

Which real-world CVEs involve LDAP injection?

Apache Syncope's CVE-2020-1953, patched in versions 2.0.14 and 2.1.14 (released April 2020), is one of the clearest documented cases: the identity management platform failed to sanitize certain filter values, allowing an attacker to construct an LDAP query that bypassed authentication against the underlying directory. Because LDAP injection usually lives in custom authentication glue code — the "connect our app to corporate AD" module every enterprise IT team writes internally — most instances never get a CVE at all; they surface in penetration test reports and bug bounty submissions instead, filed under generic "authentication bypass" or "broken access control" findings rather than CWE-90 by name. This underreporting is itself a signal for supply chain risk teams: if you rely on CVE feeds alone to find LDAP injection, you will systematically undercount it, because the vulnerability class disproportionately lives in first-party and vendor-custom code rather than the open-source packages that CVE databases track well.

How do you detect and prevent LDAP injection?

You prevent LDAP injection by escaping the five RFC 4515 special characters — *, (, ), \, and NUL (\00) — in every value before it enters a filter, and you detect it by testing every input field that feeds a directory query with those characters plus timing/boolean probes. Concretely: use your platform's built-in escaping function rather than hand-rolled string replacement (ldap_escape() in PHP, Filter.encodeValue() in Java's UnboundID SDK, or .NET's DirectoryEntry parameterized search), and treat LDAP filter construction the same way you'd treat SQL — never string-concatenate user input into a filter literal. Enforce least-privilege bind accounts so a successful injection can't enumerate the whole directory even if a filter is bypassed, and rate-limit or alert on repeated malformed-filter errors from the same source IP, since blind LDAP injection generates a distinctive pattern of near-identical requests with single-character variations. On the code-scanning side, static analysis rules that flag string concatenation feeding into javax.naming.directory.SearchControls, ldap_search(), or DirectorySearcher.FindAll() calls catch the majority of cases before they ship, which is far cheaper than finding them in a pentest after the authentication module is already in production.

How Safeguard Helps

Safeguard's reachability analysis traces whether the specific LDAP query construction pattern behind a CWE-90 finding is actually reachable from an untrusted entry point in your application's call graph, so security teams can prioritize the login-form and directory-search code paths that matter instead of triaging every string-concatenated filter in the codebase. Griffin AI reviews the surrounding code to confirm whether user input is escaped before it reaches an LDAP bind or search call and generates an auto-fix pull request that swaps raw concatenation for the correct escaping function for your language and LDAP library. Safeguard's SBOM generation and ingest pipeline also flags any LDAP client library or identity-management dependency (like Apache Syncope) with a known CWE-90 advisory the moment it enters your supply chain, so patches like the one shipped for CVE-2020-1953 get applied before an attacker finds the gap.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.