An enterprise app authenticates users against Active Directory. The login builds an LDAP search filter to find the matching account:
const filter = `(&(uid=${username})(userPassword=${password}))`;
client.search('ou=people,dc=corp,dc=com', { filter });An attacker logs in with the username *)(uid=*))(|(uid=* and any password. The filter becomes a different query than you wrote — the injected parentheses and wildcards restructure it so it matches the first account in the directory, and they're authenticated. This is LDAP injection, and it works for the same reason SQL and NoSQL injection do: user input became part of the query's structure instead of staying inside it as data.
How the filter grammar gets abused
LDAP search filters are a small prefix-notation language. (uid=jsmith) is an equality match; (&(a)(b)) is "a AND b"; (|(a)(b)) is "a OR b"; and * is a wildcard. When you paste raw input between the parentheses, the attacker gains access to all of that syntax:
*— wildcard.uid=*matches every user. A password field of*can match any stored value in a naive filter.)and(— close your clause early and open a new one, letting the attacker append their own conditions.|and&— inject OR/AND logic to loosen an AND-based auth check into something that always matches.
The canonical auth-bypass payload *)(uid=*))(|(uid=* does exactly this: it closes the username clause, injects an always-true wildcard, and rebalances the parentheses so the overall filter stays syntactically valid but semantically wide open.
Injection also enables enumeration and blind extraction. By searching for (uid=a*), then (uid=b*), and observing which queries return results, an attacker maps out valid usernames — and the same prefix trick against a sensitive attribute extracts its value one character at a time, exactly like a blind SQL injection.
The fix: escape filter input, or better, don't build filters by hand
There are two levels of defense, and you want both.
Escape every value that goes into a filter. RFC 4515 defines exactly how: each special character becomes a backslash followed by its two-digit hex code — ( → \28, ) → \29, * → \2a, \ → \5c, NUL → \00. Do not hand-roll this; use your LDAP library's escaping function, which encodes filter assertion values and distinguished names correctly.
Then verify the password by binding, not by matching it in the filter. The strongest pattern is: search for the user by their (escaped) username to find their distinguished name, then attempt an LDAP bind with that DN and the supplied password. The password never appears in a filter at all — the directory itself checks it — so there is nothing to inject on the credential side.
The principles to carry over
- Escape filter values with a library function, never by hand. RFC 4515 escaping is small but easy to get subtly wrong; use the encoder your LDAP client ships.
- Verify passwords by binding, not by filtering. Keep the credential out of the query entirely — let the directory authenticate it. This also avoids ever needing the password in a comparable form.
- Use a low-privilege service account. The account that runs the initial search should be able to read only what it needs. If injection ever broadens a search, it can't reach attributes the service account can't see.
- Scope and constrain searches. Set a narrow base DN,
SUBTREEonly where required, and a small size limit, so a widened filter can't enumerate the whole directory.
The single idea underneath all of it: an LDAP filter is code, and user input belongs in it only as escaped data — ideally not on the password path at all. Bind to check credentials; escape to search.
Try it in the LDAP injection simulation: bypass a corporate login with a wildcard payload, then switch to search-then-bind with escaping and watch the bypass close. It rounds out the injection family alongside SQL injection and NoSQL injection — same root cause, three different query languages.
Frequently Asked Questions
Related posts
Server-Side Template Injection: From {{7*7}} to RCE
SSTI happens when user input is rendered as part of a server-side template instead of passed as data. Here's how {{7*7}} becomes remote code execution, and how to render templates safely in Jinja2, Twig, and Node.js.
Aug 8, 2026 · 7 min readNoSQL Injection: When $gt and $where Bypass Your Login
NoSQL databases aren't immune to injection — they just get attacked differently. Here's how operator injection and $where JavaScript let attackers bypass auth in MongoDB, and how to stop it in Node.js and Python.
Aug 6, 2026 · 7 min readPath Traversal: Reading Files You Were Never Meant to Reach
Path traversal (directory traversal) turns a file parameter into a key to your whole filesystem. Here's how ../ attacks work, why string filtering fails, and how to canonicalize and confine paths in Node.js, Python, Java, and PHP.
Aug 5, 2026 · 8 min read