Description
LDAP injection occurs when untrusted data is interpreted as syntax, rather than a value, in a search filter or distinguished name (DN). These contexts use different grammars.
- Search filters follow RFC 4515. Characters such as
*,(,),\and NUL can change a condition. Attacker-controlled attribute names or matching rules can also change its meaning. - DNs and search bases follow RFC 4514. Separators and metacharacters such as
,,+,=and\can change the target entry or search subtree.
Filter encoding is therefore not interchangeable with DN encoding. Merely parsing a complete attacker-supplied filter or DN does not make it safe.
Potential impact
Directory permissions and the use of search results determine the impact, which may include:
- Bypassing authentication or authorization conditions
- Reading or changing unintended directory entries
- Information disclosure or resource consumption from a broader search
Remediation
1. Keep LDAP grammar fixed and supply only values
With JNDI, use a developer-controlled filter template and the filterArgs overload. String arguments are substituted with filter metacharacters escaped. JNDI placeholders can also occur in attribute or matching-rule positions, so place them only in assertion-value positions and keep attributes, matching rules, operators and placeholder positions fixed.
With Spring LDAP 4.1.1, use LdapQueryBuilder.where(...).is(value), filter(fixedTemplate, values...) or structured Filter objects. The single-argument filter(String) overload does not validate or escape its input; do not pass untrusted strings to it. LdapQueryBuilder is a Spring LDAP API, not a Spring Security API.
With UnboundID LDAP SDK, build a Filter such as Filter.createEqualityFilter("uid", value) using a fixed attribute and pass it to an object overload. With Apache Directory, use a structured API such as FilterBuilder.
2. Build DNs from components
Construct DNs and search bases from fixed attribute types and values. Suitable APIs include JNDI Rdn/LdapName, Spring LDAP LdapNameBuilder and UnboundID RDN/DN. Parsing a complete untrusted DN with new LdapName(untrustedCompleteDn) preserves its syntax rather than neutralizing it.
JNDI Context methods interpret String names using JNDI composite-name syntax as well as LDAP DN syntax. To avoid double-escaping errors, pass LdapName directly to a Name overload instead of converting it to a string. Spring LDAP LdapEncoder.nameEncode encodes LDAP DN values, not JNDI composite names; do not use it as the sole defense for a JNDI String name.
3. Apply context-specific encoding only to values
Use a maintained context-specific encoder only when a structured or parameterized API is unavailable.
- Filter assertion values: use RFC 4515 encoding, such as Spring
LdapEncoder.filterEncodeor ESAPIencodeForLDAP(value)/encodeForLDAP(value, true). - DN values: use RFC 4514 DN-value encoding separately. For JNDI calls, prefer a structured
Nameto an encoded string.
ESAPI encodeForLDAP(value, false) preserves wildcards and is not a safe substitute for an untrusted exact-match value. If wildcard searching is required, map user choices to explicit server-defined search modes rather than accepting filter syntax, and limit result count and search time.
4. Restrict syntax choices and permissions
Map user-selectable attributes, matching rules, search scopes or other grammar elements through a finite server-owned allow-list. Bind with a least-privileged account and configure client- and server-side result-size and time limits. These controls reduce impact but do not neutralize LDAP syntax.
Examples
JNDI search filters
Before
import jakarta.servlet.http.HttpServletRequest;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.DirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
NamingEnumeration<SearchResult> searchUser(
DirContext context, HttpServletRequest request) throws NamingException {
String username = request.getParameter("username");
SearchControls controls = new SearchControls();
controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
String filter = "(&(uid=" + username + ")(objectClass=person))";
return context.search(
"ou=users,dc=example,dc=com", filter, controls);
}
RFC 4515 metacharacters in username may be interpreted as filter operators or additional conditions.
After
import jakarta.servlet.http.HttpServletRequest;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.DirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
NamingEnumeration<SearchResult> searchUser(
DirContext context, HttpServletRequest request) throws NamingException {
String username = request.getParameter("username");
SearchControls controls = new SearchControls();
controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
controls.setCountLimit(100);
controls.setTimeLimit(2_000);
String fixedFilter = "(&(uid={0})(objectClass=person))";
return context.search(
"ou=users,dc=example,dc=com",
fixedFilter,
new Object[] {username},
controls);
}
The filter grammar and search base are fixed in code, and username is substituted only as an assertion value.
Building a JNDI DN
import jakarta.servlet.http.HttpServletRequest;
import javax.naming.NamingException;
import javax.naming.directory.DirContext;
import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;
Object lookupUser(DirContext context, HttpServletRequest request) throws NamingException {
String username = request.getParameter("username");
LdapName userDn = new LdapName("ou=users,dc=example,dc=com");
userDn.add(new Rdn("uid", username));
return context.lookup(userDn);
}
Rdn treats username as a DN value, and lookup(Name) does not reinterpret the structured name as a JNDI composite-name string.
Spring LDAP queries
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.ldap.query.LdapQuery;
import static org.springframework.ldap.query.LdapQueryBuilder.query;
LdapQuery userQuery(HttpServletRequest request) {
String username = request.getParameter("username");
return query()
.base("ou=users,dc=example,dc=com")
.countLimit(100)
.timeLimit(2_000)
.where("uid").is(username)
.and("objectClass").is("person");
}
The server defines the attributes and filter structure, while the builder handles username in the value context.
Review operational settings
Review directory ACLs, the binding account’s permissions and server-side search limits alongside the application code. If search results are used for authentication or authorization, review those decisions separately.
References
- CWE-90: Improper Neutralization of Special Elements used in an LDAP Query
- OWASP LDAP Injection Prevention Cheat Sheet
- OWASP Top 10:2025 A05 - Injection
- OWASP ASVS 5.0 V1.2.6
- RFC 4515: LDAP Search Filters
- RFC 4514: LDAP Distinguished Names
- Java SE 25
DirContext - Java SE 25
Rdn - Oracle JNDI Tutorial: Handling Special Characters
- Spring LDAP 4.1.1: Advanced LDAP Queries
- Spring LDAP
LdapEncoder - UnboundID LDAP SDK for Java 7.0.5
Filter - Apache Directory
FilterBuilder