LDAP Injection

LDAP Injection: When User Input Rewrites a Directory Search

LDAP injection is SQL injection's directory-service cousin. Learn how unescaped assertion values rewrite a search filter, why password checks belong in a bind, and how RFC 4515 encoding closes the boundary.

SelfSec Team6 min read
Part 1 of 1from theLDAP Injectionseries
*)(uid=*))(|(uid=*

At a glance

CWE-90
Interpreter
LDAP search-filter parser
Required condition
Untrusted values are concatenated into an LDAP filter without context-correct encoding.
Potential impact
Filter bypass, directory enumeration, and unauthorized attribute disclosure.
Primary defense
Encode assertion values for RFC 4515 and verify passwords with an LDAP bind.

Safe practice: use benign proof only, in a disposable local lab or on a system you are explicitly authorized to test.

On this page

LDAP injection occurs when untrusted input is concatenated into an LDAP search filter, letting an attacker alter the filter's logic with special characters such as the asterisk, parentheses, ampersand and pipe. By changing the intended assertion, an attacker can enumerate directory entries or coerce a lookup into returning records it was never supposed to. It is the directory-service analogue of SQL injection, and it stems from the same mistake: unencoded data crossing into filter syntax.

LDAP backs a lot of corporate identity — Active Directory, OpenLDAP, and the login forms that authenticate against them. A vulnerable lookup can expose user and group attributes or undermine an application that incorrectly treats “the search returned a user” as proof of identity.

The mental model: a filter is a small program

LDAP search filters use a prefix grammar. Parentheses create assertions, & and | combine them, ! negates them, and * is a wildcard. An assertion value is safe only when filter metacharacters are encoded for that exact context.

Intended filter Meaning
(uid=alice) Find the entry whose UID is exactly alice
(uid=*) Find every entry that has a UID
(&(uid=alice)(active=TRUE)) Find active Alice
(|(uid=alice)(uid=bob)) Find Alice or Bob

LDAP distinguished names and LDAP search filters have different escaping rules. An encoder for a DN is not automatically safe for a filter assertion, and URL encoding is not a substitute for either one.

How it happens

The trouble starts when an application drops user input straight into that grammar. This deliberately flawed login treats the existence of a directory entry as authentication and also places a password inside the search filter:

$user = $_GET['user'];
$pass = $_GET['pass'];

$filter = "(&(uid=" . $user . ")(password=" . $pass . "))";
$result = ldap_search($conn, "ou=people,dc=example,dc=com", $filter);

The intent is a filter like (&(uid=alice)(password=secret)). But user and pass are attacker-controlled, and the special characters that give LDAP filters their structure pass through unencoded. Password comparison in a search is also the wrong authentication primitive: the application should locate one distinguished name and ask the directory to verify the password with a bind.

A concrete attack: authentication bypass

The attacker submits a user value engineered to close the UID clause early and add broader filter logic:

user = *)(uid=*))(|(uid=*
pass = anything

Substituted into the template, the filter the directory actually evaluates becomes:

(&(uid=*)(uid=*))(|(uid=*)(password=anything))

The injected expression is intended to introduce wildcard assertions so the search returns an entry without proving the submitted identity. Exact parser behavior depends on the surrounding filter and LDAP server, which is why production testing should use uniquely marked, non-destructive probes rather than assuming one copied payload will be valid everywhere. In this flawed application, any unexpected entry is dangerous because a search result is incorrectly treated as a successful login.

The same lever can widen directory searches. A wildcard assertion can turn one-person lookup behavior into a list of entries, exposing attributes the endpoint was supposed to return only for an exact identity.

The attacker never guessed a credential. They rewrote the question the directory was asked, so that the answer was always "yes."

What an attacker gains

Once filter logic is under attacker control, the directory becomes an open book:

  • Authentication bypass — log in as a valid user, often an administrator, without a password.
  • Directory enumeration — list users, groups and organizational units that should be hidden.
  • Attribute disclosure — read sensitive fields such as email addresses, phone numbers, group memberships and sometimes password hashes.
  • Privilege discovery — map who belongs to which admin group, fueling the next stage of an attack.

Because directories underpin single sign-on and access control, a filter bypass here often unlocks far more than one application.

Where LDAP injection appears

Authentication is only one location. Employee directories, group membership checks, recipient pickers, address-book search, synchronization rules and administrative filters all construct LDAP expressions. Values may be usernames, email addresses, organization units or stored profile fields.

Trace each value to its context. A base DN needs DN-safe construction, while a search assertion needs RFC 4515 encoding. If an application lets a request choose an attribute name or Boolean filter structure, mapping a small allow-listed option to fixed server-owned syntax is safer than trying to encode the structure.

The fix: encode values and bind passwords

The reliable defence is to never concatenate raw input into a filter. Encode every assertion value per RFC 4515 so it is treated as literal data, and use a filter-building API that applies the same rule where one is available.

RFC 4515 requires escaping *, (, ), \ and the NUL byte as \xx hex sequences. Most platforms ship a helper:

$user = ldap_escape($_GET['user'], '', LDAP_ESCAPE_FILTER);
$filter = "(uid=" . $user . ")";
$result = ldap_search($conn, "ou=people,dc=example,dc=com", $filter, ['dn']);
$entries = ldap_get_entries($conn, $result);

if ($entries['count'] !== 1) {
    http_response_code(401);
    exit;
}

$authenticated = @ldap_bind($conn, $entries[0]['dn'], $_GET['pass']);
if (!$authenticated) {
    http_response_code(401);
    exit;
}

ldap_escape encodes *, (, ), backslash and NUL for filter use. The search must return exactly one entry, and the bind asks the directory to verify the password without placing it in a search expression. Do not suppress bind errors in production without replacing them with proper error handling; the example uses it only to keep credentials out of a client-facing warning.

.NET's standard System.DirectoryServices.Protocols surface does not provide the fictional LdapFilter.Escape helper sometimes shown in examples. Use a maintained RFC 4515 encoder or a query builder that documents filter-value encoding, and test it with all five required special cases. Never hand-roll a partial replacement that covers only * and parentheses.

Fixes that do not hold

  • URL-encoding the request: the web framework decodes it before the LDAP layer sees the value.
  • Escaping for a distinguished name: DN escaping and filter escaping protect different grammars.
  • Removing only *: parentheses, backslash, NUL and Boolean operators still affect parsing.
  • Returning one result: an injected filter can deliberately make an unexpected entry appear first.
  • Searching for a password attribute: password authentication belongs in a bind operation.

A developer review checklist

  1. Find every interpolated or concatenated LDAP filter.
  2. Separate fixed filter structure from RFC 4515-encoded assertion values.
  3. Keep attribute names and Boolean structure server-owned.
  4. Require exactly one identity result before authentication continues.
  5. Verify credentials with an LDAP bind over a protected connection.
  6. Give the directory service account access only to required subtrees and attributes.

References

How SelfSec proves this one

Scanner behaviour
Module
LDAP Injection
How it probes
Filter-breaking and wildcard payloads against directory-backed search and authentication endpoints.
How it confirms
Result-set differentials against the baseline query confirm the filter was actually altered rather than rejected.

Reported as CWE-90 · ATT&CK T1190 · SARIF 2.1.0

A reflected response alone is not enough to promote a LDAP Injection finding — the classical engine has to confirm the behaviour before it reaches the report. See the detection engine

SelfSec is intended strictly for authorized security testing of systems you own or are explicitly permitted to assess.

Do both things about LDAP Injection

SelfSec covers this class from both sides — the scanner confirms it in your own app, the firewall blocks it in front of your origin while the fix ships.

The LDAP Injection series

  1. 01OverviewLDAP Injection: When User Input Rewrites a Directory SearchReading

Related reading