July 13, 2026
LDAP Injection in PHP — The Auth Bug Nobody Tests For
LDAP injection is SQL injection’s less famous cousin. Same mechanic, less awareness, real impact. PHP defense with verified rules.

By Ann R.
18 min read
A security firm runs a penetration test on an enterprise PHP application that's been in production for six years. The app is a corporate intranet with LDAP-based authentication — users log in with their AD credentials, the PHP application queries LDAP to verify. The pentesters run their standard checks: SQL injection, XSS, CSRF, IDOR. Nothing significant. On the last day, one of them tries a set of LDAP injection payloads against the login form. Within 20 minutes, they have unauthenticated access as any user in the directory. The payload was *)(uid=*))(|(uid=* submitted as the username. The application concatenated it directly into the LDAP filter; the filter now matched any user in the directory; the authentication check trivially passed. Six years of production; hundreds of security audits (mostly automated); zero previous detection.
LDAP injection is SQL injection's less famous cousin. The concept is identical — user input concatenated into a query language, special characters interpreted as syntax rather than data — but the awareness is much lower. SQL injection appears in every security training course; LDAP injection appears occasionally. Web application scanners have deep libraries of SQL injection payloads; LDAP payload libraries are much thinner. Developers know to use prepared statements for SQL; the equivalent LDAP practice (using ldap_escape()) is less established. The vulnerability class is old (documented since the early 2000s); the specific impact and prevalence in PHP applications is under-discussed.
This article walks through what LDAP is and where PHP applications typically use it, the vulnerable pattern (string concatenation into filters), the specific special characters that make injection possible, the verified behavior of PHP's ldap_escape() function, the two escape contexts (filter vs distinguished name), safer authentication patterns, and why standard testing tools often miss LDAP injection. The goal is developers who use LDAP in PHP applications recognizing the vulnerability, preventing it in new code, and finding it in existing code.
TL;DR Speedrun
- LDAP injection is the LDAP-context equivalent of SQL injection. User input concatenated into an LDAP filter (the query language) can escape the intended query context and modify the query's logic. Attacker submits a specially crafted username or search input; the resulting filter matches unintended entries or bypasses authentication entirely.
- The vulnerable pattern:
$filter = "(uid={$username})"— direct string concatenation of user input into the filter. Submittingadmin)(objectClass=*as the username produces the filter(uid=admin)(objectClass=*)— a broken/malformed filter that some LDAP servers interpret permissively, or the more targeted*)(uid=*payload that produces(uid=*)(uid=*)— matching all users. - Five special characters need escaping in LDAP filters (RFC 4515). Verified:
\x00(null byte) escapes to\00; ( escapes to\28; ) escapes to\29; * escapes to\2a; \ escapes to\5c. Any user input in a filter must have these characters escaped or the input can modify the filter's logic. - PHP's
ldap_escape()handles this correctly. Added in PHP 5.6. Two modes:LDAP_ESCAPE_FILTERfor filter context,LDAP_ESCAPE_DNfor distinguished name context. Different modes escape different characters — filters have 5 special chars, DNs have 9 (comma, plus, equals, less, greater, hash, semicolon, backslash, quote). Using the wrong mode leaves you vulnerable in the other context. - Authentication via LDAP bind is safer than filter matching. Instead of
SELECT user WHERE uid = $submitted AND password = $submitted, use LDAP bind: connect to LDAP with the submitted credentials, if the bind succeeds, authentication is valid. Bind semantics don't have filter injection issues — the username becomes part of the DN (which still needs escaping) or is used to construct a search filter (still needs escaping), but the credential comparison happens inside the LDAP server, not in a client-constructed filter. - Symfony's LDAP component and other framework abstractions provide safer APIs.
$ldap->query(...)->execute()with parameter binding rather than string concatenation. Similar to how frameworks discourage raw SQL, they discourage raw LDAP filters. Application code that uses the framework's LDAP abstraction is much less likely to have injection bugs. - Standard web application scanners underrepresent LDAP. SQL injection has hundreds of payload variants in tools like SQLMap, OWASP ZAP, Burp Suite. LDAP injection has a handful. Applications with LDAP authentication rarely get thoroughly tested against LDAP payloads. Manual testing or specialized tools (or specific security engagements) are typically required to surface these bugs.
What You'll Learn
- Where PHP applications typically use LDAP
- The specific vulnerable pattern (string concatenation into filters)
- The five LDAP filter special characters and their escapes
- Filter escaping vs DN escaping (two different contexts)
- Authentication patterns that avoid injection entirely
What LDAP Is and Where PHP Uses It
LDAP (Lightweight Directory Access Protocol) is a protocol for accessing directory services. Directory services store hierarchical information about users, groups, computers, and other organizational entities. The most common implementation in enterprise environments is Microsoft Active Directory; open-source alternatives include OpenLDAP, 389 Directory Server, and FreeIPA.
Typical PHP application uses of LDAP:
Authentication: users log in with their corporate credentials; the PHP app verifies credentials against LDAP. This is common in enterprise intranets, HR systems, and any application deployed in corporate environments where users have AD accounts.
User directory queries: showing organizational information (who reports to whom, who's on what team, employee contact info). The PHP app queries LDAP to fetch user attributes.
Authorization: checking group membership to determine access. "Is this user in the 'Administrators' AD group?" is an LDAP query.
Single sign-on integration: LDAP is often part of larger SSO architectures. The PHP app might integrate with LDAP directly or through an intermediate protocol (SAML, OAuth) that ultimately references LDAP.
For all these use cases, the PHP application constructs LDAP queries (filters, distinguished names) and sends them to the LDAP server. If user input contributes to the query construction, injection is possible.
LDAP queries have two main components:
Filters: express which entries to return. Filter syntax uses parentheses and operators: (uid=alice) matches entries with uid attribute equal to "alice"; (&(uid=alice)(objectClass=user)) matches entries with both conditions; (|(uid=alice)(uid=bob)) matches entries with either condition. Wildcards are supported: (uid=*) matches all entries with any uid attribute.
Distinguished Names (DN): identify specific entries in the directory hierarchy. cn=alice,ou=users,dc=example,dc=com identifies the entry for "alice" in the users organizational unit of example.com's directory tree.
Both filter and DN construction can be injection points if user input is concatenated without escaping. The two contexts have different special characters, so escaping needs to be context-aware.
The Vulnerable Pattern
The naive PHP code:
$username = $_POST['username'];
$password = $_POST['password'];
$filter = "(&(uid={$username})(userPassword={$password}))";
$search = ldap_search($conn, "ou=users,dc=example,dc=com", $filter);
$entries = ldap_get_entries($conn, $search);
if (count($entries) > 0) {
// Authenticated!
login_as($entries[0]);
}$username = $_POST['username'];
$password = $_POST['password'];
$filter = "(&(uid={$username})(userPassword={$password}))";
$search = ldap_search($conn, "ou=users,dc=example,dc=com", $filter);
$entries = ldap_get_entries($conn, $search);
if (count($entries) > 0) {
// Authenticated!
login_as($entries[0]);
}The intent: search for an entry with matching username AND matching password. If found, the user is authenticated.
The flaw: $username and $password are concatenated directly into the filter. Special characters in either input change the filter's structure.
Consider what happens when the attacker submits *)(uid=* as the username:
Filter: (&(uid=*)(uid=*)(userPassword=anything))Filter: (&(uid=*)(uid=*)(userPassword=anything))Wait, that's not quite right. The parenthesis balance is off. Let me try another payload: admin)(|(1=1 (with matching parentheses added by the wrapper):
Filter: (&(uid=admin)(|(1=1))(userPassword=anything))Filter: (&(uid=admin)(|(1=1))(userPassword=anything))Now the filter says: "match entries with uid=admin AND (1=1 OR anything)". The | clause with the trivially-true 1=1 makes the whole clause match; the password check happens against anything which doesn't match. But wait — the actual behavior depends on how the LDAP server parses malformed filters.
A cleaner attack: submit *))(|(uid=* as username. The filter becomes:
(&(uid=*))(|(uid=*)(userPassword=anything))(&(uid=*))(|(uid=*)(userPassword=anything))This is two separate filters concatenated. Different LDAP servers handle this differently — some reject it, some execute the first filter and ignore the rest, some produce unexpected results.
The point isn't a specific payload; it's that user input can restructure the filter. With crafted payloads, attackers can:
- Match any user regardless of credentials (auth bypass)
- Extract information via blind boolean injection
- Cause denial of service through malformed filters
- Enumerate attribute values character by character
The prevention is escaping the special characters in user input before concatenating.
Verified: The Filter Escape Rules
RFC 4515 defines the special characters in LDAP filters. Verified via PHP 8.3.6's ldap_escape with LDAP_ESCAPE_FILTER:
null byte '\x00' → '\00'
left paren '(' → '\28'
right paren ')' → '\29'
asterisk '*' → '\2a'
backslash '\' → '\5c'null byte '\x00' → '\00'
left paren '(' → '\28'
right paren ')' → '\29'
asterisk '*' → '\2a'
backslash '\' → '\5c'Any of these characters in user input can modify the filter's meaning:
-
- matches any value (wildcard) — turns exact match into any match
- ( and ) create sub-expressions — allows adding new clauses
- \ starts escape sequences — needs to be escaped itself
\x00is a null byte that some LDAP implementations treat as string terminator — can cause weird behavior
ldap_escape replaces these characters with their hex-encoded escape sequences (\ followed by two hex digits representing the byte). The escaped output is safe to concatenate into a filter — the LDAP server treats each escape sequence as the original character but as literal data, not filter syntax.
Verified injection prevention:
Payload: 'admin)(&'
Escaped: 'admin\29\28&'
Filter with raw payload: (uid=admin)(&(...)) - becomes multiple sub-filters
Filter with escaped: (uid=admin\29\28&) - matches literal string "admin)(&"Payload: 'admin)(&'
Escaped: 'admin\29\28&'
Filter with raw payload: (uid=admin)(&(...)) - becomes multiple sub-filters
Filter with escaped: (uid=admin\29\28&) - matches literal string "admin)(&"The escaped version treats the attacker's ) and ( as data, not filter syntax. The filter matches only entries with uid literally equal to "admin)(&", which doesn't exist.
The correct code:
$username = ldap_escape($_POST['username'], '', LDAP_ESCAPE_FILTER);
$password = ldap_escape($_POST['password'], '', LDAP_ESCAPE_FILTER);
$filter = "(&(uid={$username})(userPassword={$password}))";$username = ldap_escape($_POST['username'], '', LDAP_ESCAPE_FILTER);
$password = ldap_escape($_POST['password'], '', LDAP_ESCAPE_FILTER);
$filter = "(&(uid={$username})(userPassword={$password}))";Now the filter has escaped user input; injection is prevented.
Note: the second argument to ldap_escape (empty string here) is a list of additional characters to ignore. Usually left empty. The third argument is the escape context (LDAP_ESCAPE_FILTER or LDAP_ESCAPE_DN).
Verified: The DN Escape Rules Are Different
When user input contributes to a distinguished name (DN), different characters need escaping. RFC 4514 defines DN special characters. Verified via LDAP_ESCAPE_DN:
comma ',' → '\2c'
plus '+' → '\2b'
equals '=' → '\3d'
less '<' → '\3c'
greater '>' → '\3e'
hash '#' → '\23'
semicolon ';' → '\3b'
backslash '\' → '\5c'
quote '"' → '\22'comma ',' → '\2c'
plus '+' → '\2b'
equals '=' → '\3d'
less '<' → '\3c'
greater '>' → '\3e'
hash '#' → '\23'
semicolon ';' → '\3b'
backslash '\' → '\5c'
quote '"' → '\22'DN structure uses commas as separators (cn=alice,ou=users,dc=example,dc=com); equals as attribute-value separator; other characters have specific structural meaning.
Filter escaping and DN escaping have overlapping but different special character sets:
- Common to both: backslash ()
- Filter only: (, ), *,
\x00 - DN only: ,, +, =, <, >, #, ;, "
Using the wrong escape context leaves you vulnerable in the other context:
// WRONG: filter escaping applied to DN
$dn = "cn=" . ldap_escape($username, '', LDAP_ESCAPE_FILTER) . ",ou=users,dc=example,dc=com";
// Attacker submits: alice,ou=admin — comma not escaped, changes DN structure// WRONG: filter escaping applied to DN
$dn = "cn=" . ldap_escape($username, '', LDAP_ESCAPE_FILTER) . ",ou=users,dc=example,dc=com";
// Attacker submits: alice,ou=admin — comma not escaped, changes DN structureThe correct pattern: identify whether the input is going into a filter or a DN, and use the corresponding escape mode.
Some inputs go into both. If you're using the username to search for the user and also to construct a DN for binding, escape twice with the appropriate mode each time.
Bind-Based Authentication (Safer)
The vulnerable pattern above uses filter-based authentication: search for an entry matching both username and password. The safer pattern uses LDAP bind: attempt to authenticate to LDAP with the submitted credentials; if the bind succeeds, credentials are valid.
// Step 1: Find the user's DN via search (input escaping needed)
$safeUsername = ldap_escape($_POST['username'], '', LDAP_ESCAPE_FILTER);
$search = ldap_search($conn, "ou=users,dc=example,dc=com", "(uid={$safeUsername})");
$entries = ldap_get_entries($conn, $search);
if (count($entries) === 0) {
// User doesn't exist
return false;
}
$userDN = $entries[0]['dn'];
// Step 2: Attempt to bind with the user's DN and submitted password
$bindResult = @ldap_bind($conn, $userDN, $_POST['password']);
if ($bindResult) {
// Authentication succeeded - LDAP verified the password
return true;
}// Step 1: Find the user's DN via search (input escaping needed)
$safeUsername = ldap_escape($_POST['username'], '', LDAP_ESCAPE_FILTER);
$search = ldap_search($conn, "ou=users,dc=example,dc=com", "(uid={$safeUsername})");
$entries = ldap_get_entries($conn, $search);
if (count($entries) === 0) {
// User doesn't exist
return false;
}
$userDN = $entries[0]['dn'];
// Step 2: Attempt to bind with the user's DN and submitted password
$bindResult = @ldap_bind($conn, $userDN, $_POST['password']);
if ($bindResult) {
// Authentication succeeded - LDAP verified the password
return true;
}The advantages of this pattern:
- The password is never in a filter. No injection possible via the password field.
- The LDAP server handles password verification. Correct comparison, correct handling of hashed vs plaintext storage, correct handling of account lockouts and disabled accounts.
- LDAP's authentication semantics are used correctly. LDAP is designed for authentication via bind; filter-based auth misuses it.
The remaining concern: the username still needs to be escaped for the search filter. The ldap_escape call handles that.
For applications that need to search LDAP but don't have credentials to bind first, the pattern is: use a service account (predefined DN and password stored in application config) to bind for searches, then bind again with the user's DN and submitted password for authentication.
// Bind with service account for the search
$serviceDN = 'cn=service,ou=system,dc=example,dc=com';
$servicePassword = getenv('LDAP_SERVICE_PASSWORD');
ldap_bind($conn, $serviceDN, $servicePassword);
// Search for the user
$safeUsername = ldap_escape($submittedUsername, '', LDAP_ESCAPE_FILTER);
$search = ldap_search($conn, "ou=users,dc=example,dc=com", "(uid={$safeUsername})");
$entries = ldap_get_entries($conn, $search);
if (count($entries) === 0) return false;
$userDN = $entries[0]['dn'];
// Rebind with the user's credentials
$authResult = @ldap_bind($conn, $userDN, $submittedPassword);
return $authResult;// Bind with service account for the search
$serviceDN = 'cn=service,ou=system,dc=example,dc=com';
$servicePassword = getenv('LDAP_SERVICE_PASSWORD');
ldap_bind($conn, $serviceDN, $servicePassword);
// Search for the user
$safeUsername = ldap_escape($submittedUsername, '', LDAP_ESCAPE_FILTER);
$search = ldap_search($conn, "ou=users,dc=example,dc=com", "(uid={$safeUsername})");
$entries = ldap_get_entries($conn, $search);
if (count($entries) === 0) return false;
$userDN = $entries[0]['dn'];
// Rebind with the user's credentials
$authResult = @ldap_bind($conn, $userDN, $submittedPassword);
return $authResult;This is the standard pattern for LDAP authentication in web applications. Service account for searches; user bind for authentication.
Framework Abstractions
Higher-level libraries provide safer APIs. Symfony's LDAP component is a common example:
use Symfony\Component\Ldap\Ldap;
$ldap = Ldap::create('ext_ldap', [
'host' => 'ldap.example.com',
]);
$ldap->bind($serviceDN, $servicePassword);
$query = $ldap->query(
'ou=users,dc=example,dc=com',
'(uid={username})', // template with placeholder
[
'username' => $submittedUsername, // parameter - automatically escaped
]
);
$entries = $query->execute();use Symfony\Component\Ldap\Ldap;
$ldap = Ldap::create('ext_ldap', [
'host' => 'ldap.example.com',
]);
$ldap->bind($serviceDN, $servicePassword);
$query = $ldap->query(
'ou=users,dc=example,dc=com',
'(uid={username})', // template with placeholder
[
'username' => $submittedUsername, // parameter - automatically escaped
]
);
$entries = $query->execute();The {username} placeholder in the filter template gets escaped automatically when the parameter is provided. Similar to prepared statements in SQL — the query structure is separated from the data, and the library handles escaping.
Laravel doesn't have a first-party LDAP component but several community packages provide similar patterns (adldap2/adldap2, directorytree/ldaprecord-laravel). They wrap the underlying LDAP calls with escaping and provide fluent APIs.
For teams using LDAP heavily, adopting a framework abstraction is worthwhile. The abstraction moves the escaping responsibility from every application developer to the library authors; library code is tested more thoroughly and updated when new attack techniques emerge.
For teams with occasional LDAP usage, direct use of ldap_escape is fine as long as it's applied consistently. Code review, static analysis, and testing help catch missed cases.
Attack Scenarios in Detail
Authentication bypass: The classic. Attacker submits payload as username; filter is modified to match any user; password check either bypassed or trivially satisfied. Example payload: admin)(uid=*. Depending on the specific filter template, this either produces a filter matching everything, causes the server to return an error (still exploitable if the error path doesn't fail closed), or matches admin+everything.
Blind boolean injection: Attacker doesn't get direct data leakage but can infer information through response differences. Submit username alice)(mail=a* — if a user named alice with email starting with 'a' exists, the query returns her; otherwise no entries. By iterating through characters, the attacker can enumerate the entire email field character by character. Slow but reliable.
Attribute value extraction: Similar to blind boolean, targeting specific attributes. alice)(&(objectClass=user)(department=IT — is alice in the IT department? Yes/no from the response. Iterate over possible values or over character positions.
Denial of service: Malformed filters that cause the LDAP server to use excessive resources. (&(objectClass=*)(uid=*)(cn=*)(sn=*)(mail=*)) searching the entire directory. Or filters designed to trigger specific server-side bugs. Less common attack but real.
Information disclosure via error messages: Some LDAP servers return detailed error messages that reveal information about the directory structure, attribute names, or query specifics. Even a "failed" injection attempt can leak useful reconnaissance data.
Bypass of authorization checks: If the application uses LDAP group membership queries for authorization, and those queries have user-influenced parameters, injection can grant unauthorized access. alice)(memberOf=* — matches alice as if she's in every group.
Each attack requires understanding the specific application's LDAP integration. Generic payloads may or may not work depending on filter structure, LDAP server behavior, and error handling. Successful exploitation usually requires either code access or careful blackbox testing to identify the filter template.
Why Standard Tools Miss This
Web application scanners have extensive libraries for well-known vulnerability classes. SQL injection has hundreds of payloads across dozens of database types. XSS has payload libraries covering many contexts. LDAP injection has less coverage:
Fewer payload templates. LDAP injection payloads are more application-specific — the exact payload depends on the filter template being injected into. Generic payloads work less reliably than for SQL.
Fewer error signatures. Scanners identify SQL injection partly through database error messages that appear in responses. LDAP server errors are less standardized; error signatures vary widely.
No standard test methodology. OWASP has excellent guidance on testing for SQL injection, XSS, CSRF. LDAP injection guidance exists but is less comprehensive.
Assumption that LDAP isn't user-facing. Some scanning approaches assume LDAP is internal-only. LDAP-authenticated web applications expose LDAP to user input via the auth form; scanners may not recognize the exposure.
Complex filter structures. LDAP filter templates in real applications can be complex (nested AND/OR clauses, multiple attribute checks). Determining what payload would exploit the filter requires understanding the template — which the scanner doesn't have.
The practical implication: automated scanning is unlikely to find LDAP injection in your application. Manual testing (or specialized security engagements) is typically required. Code review is often more reliable than dynamic testing — grep for ldap_search calls, examine what's concatenated into the filter, verify escaping.
Pitfalls to Avoid
Concatenating user input into LDAP filters without escaping. The core vulnerability. Always use ldap_escape (or framework abstractions with automatic escaping) on any user input in filters.
Using the wrong escape context. LDAP_ESCAPE_FILTER for filters; LDAP_ESCAPE_DN for distinguished names. Wrong context leaves you vulnerable in the correct context — the filter is safe but the DN isn't, or vice versa.
Assuming input validation prevents injection. Input validation (checking username format, length limits, etc.) helps but isn't sufficient. Attackers craft inputs that pass validation but still contain filter special characters. Escape after validation; don't rely on validation alone.
Filter-based password checking. Comparing passwords via filter matching ((userPassword={$password})) is architecturally wrong. Use LDAP bind for authentication; the password is never in a filter.
Not using bind-based authentication. Some code checks if a user exists via LDAP search, then verifies the password by comparing to an attribute value fetched from LDAP. This has both security issues (password comparison in application code) and correctness issues (password may be hashed in LDAP, and the format may not match your application's hash format). Use bind.
Storing LDAP passwords in the application. Service account credentials should be in environment variables or secret managers, not in code or config files committed to version control. Password rotation should be easy.
Not logging failed authentication attempts. Failed LDAP binds may indicate injection attempts, brute force attacks, or misconfigured clients. Log them; monitor for anomalies.
Using anonymous bind for searches. Some LDAP servers allow anonymous binding, which lets any user (including unauthenticated) read directory contents. Even without injection, anonymous access is often too permissive. Configure the LDAP server to require authenticated binding for reads.
Ignoring the possibility of injection in DN construction. DN construction is less common than filter construction but does happen. Any user input that contributes to a DN needs LDAP_ESCAPE_DN treatment.
Mini Q&A
How common is LDAP injection in production applications?
Harder to quantify than SQL injection because it's less studied. Enterprise applications with LDAP authentication that were written before ldap_escape was widely known (pre-2014 codebases) have a reasonable probability of being vulnerable. Modern applications built with framework LDAP components are usually safer. Manual pentesting engagements do find LDAP injection with some regularity in enterprise applications.
Is LDAP injection as serious as SQL injection?
Impact depends on what LDAP is used for. If LDAP is the primary authentication mechanism for the application, LDAP injection means authentication bypass — very serious. If LDAP is used only for reading organizational data (org chart, contact info), the impact is information disclosure without direct authentication impact. In any case, unauthenticated code execution potential is generally lower than SQL injection (LDAP queries don't have the same "arbitrary command" potential as SQL) but authentication bypass is a serious outcome.
Does using Active Directory instead of OpenLDAP prevent this?
No. LDAP injection is a client-side (application-side) vulnerability. The LDAP server (AD, OpenLDAP, whatever) receives whatever filter the application sends. If the application constructs the filter unsafely, injection works regardless of server implementation. Different servers may have slightly different behavior for malformed filters, but the core vulnerability is in the application code.
What about LDAPS (LDAP over TLS)?
Doesn't prevent injection. LDAPS encrypts the connection between the application and the LDAP server, protecting against network-level eavesdropping. It doesn't affect how the application constructs queries or how the LDAP server processes them. Use LDAPS for network security AND ldap_escape for query safety — different concerns, both needed.
Should I write custom escaping instead of using ldap_escape?
No. ldap_escape is tested, correct, and handles both filter and DN contexts. Custom escaping is error-prone — it's easy to miss characters, get the encoding wrong, or introduce subtle bugs. Use the built-in function.
How do I test my application for LDAP injection?
Code review is most reliable: search for ldap_search, ldap_list, ldap_read calls; examine what's concatenated into the $filter argument; verify each user input is escaped via ldap_escape. Dynamic testing with payloads (submit *)(uid=* type strings as usernames, observe behavior) can identify vulnerabilities but may miss subtle ones. Framework audit tools may flag string concatenation into filter contexts.
What if I need to search for a partial match (like an autocomplete)?
Legitimate wildcard usage means you want to include * in the filter — but the wildcard should come from the application code, not from user input. (uid=" . ldap_escape($input, '', LDAP_ESCAPE_FILTER) . "*) — the * at the end is application code; the user input is escaped so any * in it becomes literal. The filter searches for entries starting with the user's input.
Are LDAP prepared statements a thing?
Not directly in the PHP LDAP API. PHP's ldap_search accepts a fully-formed filter string. Some LDAP client libraries provide higher-level query builders (Symfony's LDAP component, ldaprecord-laravel) that internally escape parameters — analogous to prepared statements but implemented at the client library level.
Wrap-Up
LDAP injection is real, exploitable, and undertested. Applications with LDAP authentication that predate ldap_escape (or that were written by developers unfamiliar with LDAP injection) have a real probability of being vulnerable. Automated scanning tools rarely find these bugs; manual review of LDAP-touching code is the most reliable detection method.
For PHP applications with LDAP integration, the practical work is: audit existing code for filter construction; verify all user input in filters goes through ldap_escape with the correct context; migrate to framework abstractions (Symfony LDAP component, ldaprecord-laravel) where feasible; use bind-based authentication rather than filter-based password checking. Each step is bounded; the collective impact is significant.
For teams building new PHP applications with LDAP, the recommendations are: use a framework abstraction from the start; use bind-based authentication; treat LDAP filters like SQL queries (never concatenate user input); include LDAP-specific tests in your security testing suite.
The deeper takeaway: injection vulnerabilities exist wherever a query language meets untrusted input. SQL is the most famous case; LDAP is a less-famous case with similar mechanics; XPath, NoSQL query languages, template languages, and shell commands all have equivalent vulnerabilities. Recognizing the pattern ("this is a query language, this is user input, they're being combined") is a security skill that transfers across contexts. The specific escape function or parameter binding mechanism varies; the general defensive posture is the same.
Closing Loop
The enterprise application whose pentest revealed LDAP injection eventually gets a comprehensive remediation. The team audits every LDAP integration in the codebase: authentication, user search, group membership checks, org chart queries. About 40 places construct LDAP filters or DNs; roughly half concatenate user input without escaping. Each is fixed to use ldap_escape with the correct context; authentication is migrated to bind-based; a service account credential rotation happens as part of the cleanup.
The team's security testing suite gains LDAP-specific payload tests. The tests submit known injection payloads to the login form, group membership check endpoints, and any other user-input-to-LDAP path; the tests verify that authentication and authorization work correctly with each payload (i.e., the payloads don't grant unauthorized access). The tests catch regressions in future changes.
Six months later, an unrelated pentest engagement finds no LDAP-related issues. The pentesters comment specifically that the LDAP integration is unusually clean for an enterprise application. The team's remediation work included documentation and knowledge transfer; new team members learn the LDAP escaping rules during onboarding. The pattern of "specific vulnerability class + specific defenses + tests to prevent regression" continues to reduce security incidents in the domain.
A year later, the team encounters a similar situation with XPath queries (used for XML parsing in a subsystem). Someone recognizes the pattern immediately: query language + user input = potential injection. The team applies the same posture: identify the queries, identify the user inputs, use library-provided parameterization, add tests. The general defense-in-depth muscle developed for LDAP transfers to XPath. The team's engineering culture around "query language safety" continues to mature.
"People Also Ask"
1. What is LDAP injection and how does it work? LDAP injection is a vulnerability where user input is concatenated into an LDAP filter or distinguished name without proper escaping, allowing an attacker to modify the query's structure. The classic pattern: (uid={$username}) where $username comes from user input. Submitting a specially crafted value like admin)(uid=* changes the filter to match unintended entries or bypass authentication entirely. The vulnerability is analogous to SQL injection but in LDAP query syntax; the impact is authentication bypass or information disclosure, depending on how the application uses LDAP.
2. What special characters need to be escaped in LDAP filters? Five characters per RFC 4515: null byte (\x00), left parenthesis ((), right parenthesis ()), asterisk (*), and backslash (). Verified via PHP's ldap_escape with LDAP_ESCAPE_FILTER: each is escaped to a backslash-hex sequence (\28 for (, \29 for ), \2a for *, \5c for , \00 for null byte). Distinguished names use a different set (commas, plus, equals, less/greater, hash, semicolon, backslash, quote). Use the correct escape context for each.
3. How do I prevent LDAP injection in PHP? Use ldap_escape($input, '', LDAP_ESCAPE_FILTER) for user input in filters. Use ldap_escape($input, '', LDAP_ESCAPE_DN) for user input in distinguished names. Better: use a framework abstraction (Symfony LDAP component, ldaprecord-laravel) that handles escaping automatically via parameter binding. For authentication, use bind-based authentication (attempt to authenticate to LDAP with the user's credentials) rather than filter-based password matching. Never concatenate user input directly into LDAP filters or DNs.
4. What's the difference between LDAP filter escaping and DN escaping? Different special characters. Filter escaping (RFC 4515): null byte, parentheses, asterisk, backslash. DN escaping (RFC 4514): comma, plus, equals, less/greater, hash, semicolon, backslash, quote. The common character is backslash; the others are context-specific. Using the wrong escape function leaves you vulnerable in the other context. PHP's ldap_escape supports both via the third argument (LDAP_ESCAPE_FILTER or LDAP_ESCAPE_DN).
5. Is bind-based LDAP authentication safer than filter-based? Yes, generally. Filter-based authentication (searching for a user with matching username AND password in one filter) puts the password into an LDAP filter, creating an injection surface. Bind-based authentication attempts to connect to LDAP with the submitted credentials — LDAP verifies the password internally, no filter injection possible for the password field. Bind-based also correctly handles password hashing (LDAP servers store passwords hashed; comparison happens server-side), account states (disabled, locked), and other authentication semantics. The username still needs to be escaped for the search that locates the user's DN.
6. Why don't security scanners find LDAP injection? Several reasons. LDAP injection payloads are more application-specific than SQL injection payloads — the exact payload depends on the filter template. LDAP server error messages vary more than SQL error messages, making error-based detection less reliable. Scanners' LDAP payload libraries are much smaller than their SQL libraries. Many scanning tools assume LDAP is internal-only and don't test LDAP-exposed endpoints thoroughly. The practical implication: automated scanning is unlikely to find LDAP injection; code review and manual testing are typically required.
7. What LDAP libraries in PHP handle escaping automatically? Symfony LDAP component (symfony/ldap) uses parameter binding in its query API — the filter template has {placeholder} markers, and parameters are escaped automatically when the query executes. directorytree/ldaprecord-laravel provides a fluent query builder for Laravel that internally handles escaping. adldap2/adldap2 (predecessor to LdapRecord) similarly abstracts over raw LDAP calls. Using these libraries instead of direct ldap_search calls with string-concatenated filters eliminates the injection risk for typical usage.
8. Does using LDAPS (LDAP over TLS) prevent injection? No. LDAPS encrypts the network connection between the application and the LDAP server, protecting against network-level eavesdropping and man-in-the-middle attacks. It doesn't affect how the application constructs LDAP queries. If the application concatenates user input into filters without escaping, the injection works regardless of whether the connection is encrypted. LDAPS is essential for network security but is separate from query-level security. Both are needed: LDAPS for network protection, ldap_escape (or framework abstractions) for query safety.
Note: All LDAP behaviors described in this article were verified against PHP 8.3.6 with the LDAP extension. The ldap_escape function was added in PHP 5.6; earlier versions require manual escape implementations (which are error-prone and should be avoided when possible — upgrade PHP). RFC 4515 (filter syntax) and RFC 4514 (DN syntax) define the special characters; these RFCs are stable. Specific LDAP server behavior for malformed filters varies (Active Directory, OpenLDAP, 389 Directory Server, FreeIPA all handle edge cases differently); some servers are more permissive than others. Framework LDAP component behaviors reflect current Symfony LDAP component and community library conventions; specific version details should be verified against library documentation. For production security assessments, comprehensive testing against your specific LDAP integration is the reliable way to identify injection vulnerabilities; theoretical understanding of the vulnerability class is a starting point, not a substitute for testing.