Skip to content

LDAP injection

CWE-90OWASP A03:2021Updated August 31, 20267 min read

LDAP injection is a vulnerability that lets an attacker smuggle filter metacharacters such as a parenthesis or an asterisk into a directory search, because the application never separates input from filter syntax. A payload like *)(uid=* can bypass the login check or dump the entire user directory. The fix is escaping every filter value per RFC 4515 and verifying passwords with a real LDAP bind instead of a search.

LDAP injection is the directory equivalent of SQL injection: less famous, but in a corporate network it usually sits much closer to the crown jewels. An application that builds its search filter by gluing together text from an input field hands the attacker partial control over the question being asked of the user directory. Here is what the vulnerability is, how an attack unfolds, and how to close it for good.

What is LDAP injection?

LDAP injection is a vulnerability that lets an attacker inject their own filter metacharacters into a directory query made by your application. LDAP stands for Lightweight Directory Access Protocol, the protocol applications use to query a directory such as Active Directory or OpenLDAP for users, groups and permissions. The moment user input reaches a search filter unescaped, the visitor gets a say in what is actually being asked.

An everyday comparison: you hand the receptionist a note asking for the file of an employee named Jansen. On the way, somebody adds “or actually everyone’s” to the end of the note. The receptionist reads the sentence exactly as written and wheels out the whole cabinet. A directory behaves the same way: it runs the filter it receives, with no idea which part came from your application and which part came from a visitor.

What makes LDAP susceptible is the filter syntax itself. A search filter is built from parentheses, logical operators such as &, | and !, comparison operators and the wildcard *. Every one of those is an ordinary punctuation character that a user can type freely into a name, an email address or a department field. Leave them unneutralised and the user is co-authoring the logic of the query, not just supplying a value for it.

How does an LDAP injection attack work?

The problem starts in the familiar way: the filter is assembled as a string. Consider a login form that puts the username and the password into a single filter and treats any match as a successful sign-in.

Vulnerable:

// User input is pasted straight into the LDAP filter
const username = req.body.username;
const password = req.body.password;

const filter =
  "(&(uid=" + username + ")(userPassword=" + password + "))";

const matches = await search(client, "ou=users,dc=example,dc=com", filter);
if (matches.length > 0) {
  // access granted
}

For an ordinary employee entering a name and a password, this behaves as intended. An attacker, however, does not type a name into the username field: they type *)(uid=*))(|(uid=* and any password at all. The filter the directory ends up parsing becomes:

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

The attacker has closed your parentheses early and wrapped a construction of their own around them. What comes first is (&(uid=*)(uid=*)), a condition that is true for every object that has a uid. The password check has been pushed into the text that follows the first complete filter. Many directory servers parse that first filter and ignore the remainder, while others reject the request outright. Where the first behaviour applies, the password check has simply evaporated: the search returns matches, the application concludes that the sign-in was valid, and the attacker is in, usually as whichever account happens to be first in the result list.

Even short of a full bypass, this flaw pays off. A single asterisk turns (uid=jansen) into (uid=*) and returns the complete staff list in one request. And with a filter such as (&(uid=admin)(description=a*)) an attacker can guess attribute values character by character, as long as the application visibly responds differently to a match than to zero results.

The structural fix has two halves. First, escape every value that goes into a filter according to RFC 4515, the standard that defines how *, (, ), the backslash and the null byte are encoded inside a filter value. Second, do not verify the password with a search: use a bind. Look up the user’s DN (distinguished name) with a service account, then let the directory itself decide whether the supplied password belongs to that DN.

Secure:

// Every value that enters a filter is escaped per RFC 4515
function escapeFilterValue(value) {
  return String(value)
    .replace(/\\/g, "\\5c")
    .replace(/\*/g, "\\2a")
    .replace(/\(/g, "\\28")
    .replace(/\)/g, "\\29")
    .replace(/\0/g, "\\00");
}

// 1. Search with a service account, purely to resolve the DN
await client.bind(SERVICE_DN, SERVICE_PASSWORD);

const filter =
  "(&(objectClass=person)(uid=" + escapeFilterValue(req.body.username) + "))";

const matches = await search(client, "ou=users,dc=example,dc=com", filter);
if (matches.length !== 1) {
  throw new Error("Login failed");
}

// 2. Let the directory verify the password itself
const password = req.body.password;
if (password.length === 0) {
  throw new Error("Login failed");
}

const userClient = createClient();
await userClient.bind(matches[0].dn, password); // throws on a wrong password

Now *)(uid=*))(|(uid=* is nothing more than an odd username that matches no account: the parentheses and asterisks arrive in the filter as \28, \29 and \2a and leave the structure untouched. The password also never leaves the application as part of a search again, so there is nothing left to manipulate on that side.

Reject an empty password explicitly before you bind. Many directory servers treat a bind with no password as an anonymous bind and report success, which means your login check can still be bypassed with an empty field.

What is the impact of LDAP injection?

Severity depends on what the service account is allowed to see and on what the application does with the result. At the lower end sits information disclosure: a wildcard returns the full staff directory, complete with email addresses, phone numbers, job titles and the reporting structure. That is a privacy incident in its own right, and it hands an attacker excellent material for targeted phishing and social engineering.

At the upper end sits the authentication bypass shown above. Anyone who gets in without valid credentials inherits the rights of whichever account the application settles on, and with an unsorted filter that is often arbitrary or, worse, the first administrative account in the tree. An attacker can also manipulate filters that check group membership and grant themselves authorisations they were never meant to have. If the application reads sensitive attributes, those come within reach as well, up to and including password hashes where the service account is permitted to read them.

Rarer, but not unheard of, is an application that performs modifications on a DN the user can influence. The risk then shifts from reading to writing: changing group membership, resetting attributes, altering contact details that other systems trust. Because the range runs from reading an address book to taking over accounts, we rate the severity between medium and high.

How do you detect LDAP injection?

The quickest test is a single asterisk. Enter * in every field that eventually reaches a directory and watch whether the result count jumps or accounts appear that you should not be able to see. Then submit a single opening parenthesis. An error about an invalid search filter, a blank page or an HTTP 500 tells you your input is reaching the filter syntax rather than being handled as a plain value.

# Same endpoint, two searches: a name and a wildcard
curl -s "https://app.example.com/search?name=jansen" | wc -l
curl -s "https://app.example.com/search?name=%2A" | wc -l

If the application responds without an error, a tester continues blind: they compare the response to (&(uid=jansen)(description=a*)) against a condition that is certainly false and infer from the difference whether the filter is being executed. Automated scanners catch the obvious cases but miss this vulnerability regularly, because LDAP errors are usually swallowed in the application layer and the only remaining signal is a behavioural difference. Manual work is decisive here, and this is exactly the kind of weakness AssistSec looks for and demonstrates reproducibly during a penetration test.

How do you prevent LDAP injection?

  • Escape every filter value according to RFC 4515 using the standard helper from your LDAP library rather than a routine you wrote yourself.
  • Verify passwords with a bind, never with a filter. Resolve the DN first, then bind as that user and let the directory make the decision.
  • Avoid assembling distinguished names by hand. Where you must, apply RFC 4514 escaping, which treats different characters as special, including the comma, the plus sign and the equals sign.
  • Validate input against a strict allowlist. A username or a department code has a predictable format; reject anything that does not fit it.
  • Give the service account minimal privileges. Restrict the search base, restrict the attributes returned, and grant read access only where it is genuinely needed.
  • Cap the number of search results and log the searches that hit the cap, so a wildcard sweep stands out immediately.
  • Never surface raw LDAP errors to the user; log them internally instead. Error messages tell an attacker exactly how your filter is built.
  • Have login and search functionality tested regularly through a penetration test and a code review, because this is precisely the vulnerability that survives automated scanning.

Sources

Frequently asked questions

What is the difference between LDAP injection and SQL injection?

The root cause is the same: input that ends up being read as part of a command rather than as a value. The target differs. SQL injection hits the database, LDAP injection hits the directory that stores your users, groups and permissions. Because that directory is usually the organisation's central identity source, a successful LDAP injection lands directly on authentication.

Is LDAP injection still relevant in modern applications?

Yes. Intranet portals, VPN front ends, print and badge systems and plenty of older web applications still query Active Directory or OpenLDAP directly. The flaw shows up most often in hand-built search and login screens where the filter is assembled with string concatenation.

Is escaping input enough to stop LDAP injection?

Escaping per RFC 4515 closes the search filter itself and is the single most important control. It does not fix a login check that compares the password inside a filter, and it does not cover a distinguished name you assemble yourself, which follows the different escaping rules of RFC 4514. Combine escaping with a bind-based password check.

How do I test whether my application is vulnerable to LDAP injection?

Enter a single asterisk in every field that eventually reaches a directory and see whether the result count jumps or unexpected accounts appear. Then submit a single opening parenthesis: an error about an invalid search filter, an empty page or an HTTP 500 suggests your input reaches the filter syntax. Confirm any suspicion with a controlled payload in a test environment.

Related articles

Press / to search · Esc