LDAP injection

LDAP injection

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.filterEncode or ESAPI encodeForLDAP(value) / encodeForLDAP(value, true).
  • DN values: use RFC 4514 DN-value encoding separately. For JNDI calls, prefer a structured Name to 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

java
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

java
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

java
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

java
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