Web Security

LDAP Injection: How It Works and How to Prevent It

Learn how LDAP injection exploits directory query syntax, enables authentication bypass, and how parameterized queries and input sanitization stop it.

March 9, 20266 min readShipSafer Team

LDAP Injection: How It Works and How to Prevent It

LDAP injection is a server-side injection attack targeting applications that construct LDAP (Lightweight Directory Access Protocol) queries from user-supplied input. LDAP is widely used for authentication and directory lookups in enterprise environments — Active Directory, OpenLDAP, and similar directory services all use the LDAP protocol. When user input is concatenated directly into an LDAP filter without sanitization, attackers can manipulate the filter logic to bypass authentication, extract directory information, and enumerate users.

How LDAP Filter Syntax Works

LDAP filters use a prefix notation with parentheses. A basic authentication query looks like this:

(&(uid=jsmith)(password=secret123))

This filter says: find entries where uid equals jsmith AND password equals secret123. If both conditions are true, authentication succeeds.

LDAP filter special characters include:

  • ( and ) — begin and end of a filter component
  • * — wildcard matching any sequence of characters
  • & — logical AND
  • | — logical OR
  • ! — logical NOT
  • \ — escape character
  • NUL — null byte

Authentication Bypass

Consider a login form where the username and password are inserted into an LDAP filter:

# Vulnerable Python code
def authenticate(username: str, password: str) -> bool:
    ldap_filter = f"(&(uid={username})(password={password}))"
    results = ldap_conn.search_s("dc=example,dc=com", ldap.SCOPE_SUBTREE, ldap_filter)
    return len(results) > 0

An attacker submits:

  • Username: *
  • Password: *

The resulting filter becomes (&(uid=*)(password=*)), which matches every user in the directory. Authentication succeeds for any account.

A more targeted bypass:

  • Username: jsmith)(&
  • Password: anything

The resulting filter becomes (&(uid=jsmith)(&)(password=anything)). Because (&) is always true in LDAP (an AND with no conditions evaluates to true), this collapses to (&(uid=jsmith)(TRUE)) — authentication succeeds for jsmith without knowing the password.

Extracting Directory Information

Beyond authentication bypass, LDAP injection can be used for information extraction. An attacker who can enumerate which filter conditions are true can extract attribute values character by character, similar to blind SQL injection.

For example, if the application reveals whether a search returned results, an attacker can probe:

(uid=admin)(mail=a*))  — does admin's email start with 'a'?
(uid=admin)(mail=b*))  — does admin's email start with 'b'?

By iterating through possible values, the attacker can enumerate email addresses, phone numbers, group memberships, and other directory attributes.

Preventing LDAP Injection: Input Sanitization

The OWASP LDAP Injection Prevention Cheat Sheet defines two escaping functions — one for filter values (DN components use different escaping).

Escape LDAP filter special characters:

import re

def escape_ldap_filter(value: str) -> str:
    """
    Escape special characters in LDAP filter values per RFC 4515.
    """
    escape_map = {
        '\\': r'\5c',
        '*':  r'\2a',
        '(':  r'\28',
        ')':  r'\29',
        '\0': r'\00',
    }
    pattern = re.compile('|'.join(re.escape(k) for k in escape_map))
    return pattern.sub(lambda m: escape_map[m.group(0)], value)


def authenticate(username: str, password: str) -> bool:
    safe_username = escape_ldap_filter(username)
    safe_password = escape_ldap_filter(password)
    ldap_filter = f"(&(uid={safe_username})(userPassword={safe_password}))"
    results = ldap_conn.search_s("dc=example,dc=com", ldap.SCOPE_SUBTREE, ldap_filter)
    return len(results) > 0

With this escaping, a * in the username becomes \2a and is treated as a literal asterisk, not a wildcard.

Java (using FilterBuilder from UnboundID LDAP SDK):

import com.unboundid.ldap.sdk.Filter;

public boolean authenticate(String username, String password) throws LDAPException {
    // Filter.createEqualityFilter escapes special characters automatically
    Filter uidFilter = Filter.createEqualityFilter("uid", username);
    Filter pwdFilter = Filter.createEqualityFilter("userPassword", password);
    Filter combined = Filter.createANDFilter(uidFilter, pwdFilter);

    SearchRequest request = new SearchRequest(
        "dc=example,dc=com",
        SearchScope.SUB,
        combined
    );

    SearchResult result = ldapConnection.search(request);
    return result.getEntryCount() > 0;
}

UnboundID's Filter.createEqualityFilter() handles escaping internally, giving you parameterized LDAP queries analogous to prepared statements in SQL.

Node.js (using ldapts):

import { Client } from 'ldapts';
import { EscapeFilter } from 'ldapts';

async function authenticate(username: string, password: string): Promise<boolean> {
  const client = new Client({ url: 'ldap://directory.example.com' });

  await client.bind('cn=app,dc=example,dc=com', 'app-password');

  // ldapts escapes filter values when using the filter object API
  const { searchEntries } = await client.search('dc=example,dc=com', {
    filter: `(&(uid=${EscapeFilter(username)})(objectClass=person))`,
    scope: 'sub',
  });

  if (searchEntries.length === 0) return false;

  // Verify password by attempting a bind as the user
  try {
    await client.bind(searchEntries[0].dn, password);
    return true;
  } catch {
    return false;
  } finally {
    await client.unbind();
  }
}

Note the bind-based password verification pattern: instead of including the password in the search filter (which requires storing passwords in a searchable attribute), this approach performs a separate bind operation as the user. If the bind succeeds, the password is correct. This is the standard LDAP authentication pattern and eliminates password filter injection entirely.

Preventing LDAP Injection: DN Escaping

Distinguished Names (DNs) use a different set of special characters than filter values. If you construct a DN from user input, escape separately:

def escape_dn_value(value: str) -> str:
    """
    Escape special characters in LDAP DN components per RFC 4514.
    """
    dn_escape_map = {
        ',':  r'\,',
        '+':  r'\+',
        '"':  r'\"',
        '\\': r'\\',
        '<':  r'\<',
        '>':  r'\>',
        ';':  r'\;',
        '#':  r'\#',
        '=':  r'\=',
    }
    result = value
    for char, escaped in dn_escape_map.items():
        result = result.replace(char, escaped)
    return result

Input Validation Beyond Escaping

Escaping is the primary technical control. Combine it with input validation for defense in depth:

  • Username fields should only accept characters valid in your directory's naming attributes. For most organizations, a username contains only alphanumeric characters, dots, hyphens, and underscores. Reject anything outside that character set before constructing any query.
  • Enforce length limits. LDAP attributes have maximum lengths. A username field accepting 10,000 characters is already suspicious — validate that input lengths are within expected bounds.
  • Use a dedicated bind account with minimal permissions. The LDAP service account used by your application should only have read access to the attributes it needs. It should not have access to write directory entries or read sensitive attributes like password hashes.

Testing for LDAP Injection

Common test payloads to verify your application is protected:

InputExpected behavior
*Should not return all users
`)(uid=))((uid=*`
admin)(&Should not authenticate without password
\2a (literal backslash-2-a)Should be treated as literal characters

Tools like OWASP ZAP and Burp Suite include LDAP injection scanning modules. For manual testing, submit LDAP metacharacters in any field that drives a directory lookup and observe whether the response changes in ways that suggest filter manipulation.

Check Your Security Score — Free

See exactly how your domain scores on DMARC, TLS, HTTP headers, and 25+ other automated security checks in under 60 seconds.