Skip to content

CWE-90: Improper Neutralization of Special Elements used in an LDAP Query ('LDAP Injection')

Overview

LDAP Injection occurs when untrusted input is concatenated into an LDAP filter or Distinguished Name without context-correct escaping, so the value is read as query syntax rather than as data and an attacker can change what the query matches.

Relationship to Other CWEs

CWE-90 is a child of CWE-943 (Improper Neutralization of Special Elements in Data Query Logic), the general weakness behind injection into any structured query language, including SQL, NoSQL, and LDAP. Use CWE-943 for the general query-injection defense pattern (parameterized, structured query construction); use this page for LDAP-specific guidance - escaping filter values per RFC 4515, handling Distinguished Names per RFC 4514, and preferring search-then-use-returned-DN over constructing DNs from input.

OWASP Classification

A05:2025 - Injection

Risk

High: An unescaped value can turn an equality test into one that matches every entry, so a search scoped to a single account returns the whole directory, and a filter that compares a password matches any account that has one set.

Remediation Steps

Core Principle: Never concatenate untrusted input into an LDAP filter or DN; build it with the structured API or the escaping function for that context.

Trace the Data Path

Work out how untrusted data reaches the LDAP query:

  • Source: Where untrusted data enters (user input, external file, database, network request)
  • Sink: LDAP query construction or search function
  • String concatenation: The untrusted value is embedded directly in the filter or DN, with no context-specific escaping between source and sink

Use a Structured Builder or Context-Correct Escaping (Primary Defense)

Stop assembling filters and DNs by concatenation. No LDAP client parameterizes at the protocol level the way a database driver does - a filter travels as a structure the client builds from the string you hand it, so every "parameterized" LDAP API is really escape-on-substitution. That still removes the defect, because it removes the chance to forget. What it looks like depends on the ecosystem:

  • Java has a placeholder overload, DirContext.search(name, "(uid={0})", args, controls), which escapes each argument as it substitutes; and LdapName/Rdn for building distinguished names
  • Node.js has structured builders - an escapeFilter tagged template for filters, a DN class for distinguished names
  • Python's python-ldap has the same shape as Java's, ldap.filter.filter_format("(uid=%s)", [username]); ldap3 escapes one value at a time with escape_filter_chars()
  • C# has no equivalent in the framework, so the fix there is allowlist validation plus an escaping function for the context in question
  • Follow the secure code examples in the language-specific guidance rather than assuming a placeholder syntax exists

Escape for the Right Context

Filter values and DN components have different rules, and neither escaper is safe to use for the other's context:

  • Filter values (RFC 4515): escape *, (, ), \ and NUL as backslash-hex
  • DN components (RFC 4514): escape ,, +, ", \, <, >, ;, =, NUL, a leading #, and a leading or trailing space
  • Use the escaping function your ecosystem provides for each context, not one "escape LDAP" helper for both
  • Apply escaping where the query is constructed, and never rely on client-side validation

Validate and Sanitize Input (Defense in Depth)

Input validation is a second layer, not a replacement for escaping:

  • Validate untrusted data against the format you expect: a username pattern, an email address
  • Use allowlists for enumerated values
  • Reject or sanitize unexpected characters
  • Apply length limits to prevent DoS

Apply Least Privilege to Directory Accounts

Bound what an injection can reach if escaping is missed somewhere:

  • Bind with an account that has only the permissions the application needs: read-only for search operations, never an admin or root account
  • Restrict access to sensitive directory attributes
  • Use separate accounts for different application functions

Monitor and Test

Verify your fixes and enable detection:

  • Test with a bare * in each interpolated value. It is the payload that works against the common single-term filter: unescaped it turns (uid=alice) into (uid=*), a presence test matching every entry, and in a filter that compares a password it matches any account that has one set
  • The payload most write-ups quote, *)(uid=*))(|(uid=*, needs a compound filter and a server that parses the first filter and ignores the rest. Interpolated into a single-term filter it produces two top-level filters, which the JNDI, ldapts, ldap3 and .NET clients all reject in their own parsers before contacting the server - so an unescaped run raises rather than leaking, and the test cannot distinguish a working fix from a broken filter. Use it against the fixed code, where it should arrive as one literal assertion value
  • Log all LDAP queries and failed authentication attempts
  • Alert on anomalous query patterns
  • Re-test with the input from the original finding and confirm it no longer changes the query
  • Confirm legitimate lookups still return the expected entries
  • Re-scan with the security scanner to confirm the issue is resolved

Common Vulnerable Patterns

  • Directly embedding untrusted data in LDAP filters
  • Accepting arbitrary input for search or bind operations

Unescaped User Input in LDAP Filter (Pseudocode)

# Dangerous: user input in LDAP filter
search_filter = f"(uid={user_input})"

Why this is vulnerable: The filter is parsed as an expression tree, so a value containing ) and ( does not become part of the term it was interpolated into - it ends that term and starts another. A * is enough on its own to convert an equality test into a wildcard that matches every entry.

The sketch is language-neutral because every ecosystem has the same two escaping problems and they are not interchangeable: filter values follow RFC 4515 and DN components follow RFC 4514. Language pages name the encoder for each; the point that survives translation is that concatenating into either is the flaw, and a single "escape LDAP" helper is usually a sign that only one of them was considered.

Secure Patterns

LDAP Filter Escaping (Pseudocode)

# Safe: use parameterized query or escape input
safe_input = escape_filter_chars(user_input)  # Escapes *, (, ), \, NUL
search_filter = f"(uid={safe_input})"

Why this works:

  • The special characters (*, (, ), \, NUL) reach the server as literal data, not filter syntax
  • In a compound filter, the value can no longer close its own term and add another, such as (uid=*)(|(password=*))
  • A bare * no longer becomes a wildcard that enumerates every directory entry
  • Attackers can't modify query logic to access unauthorized user accounts or data

Common Pitfalls

  • Reusing filter-escaping for DN values (or vice versa): Escaping a value with LDAP filter-escaping rules and then using that same value inside a Distinguished Name, or the reverse - filter escaping and DN escaping cover different special-character sets, so a value escaped for one context still carries unescaped metacharacters for the other.
  • Blocklisting only the "obvious" characters: Stripping or rejecting * and ( but leaving ), \, or NUL untouched - a partial blocklist still leaves enough filter syntax available to close one clause and open another.
  • Escaping input but still building the DN from it: Properly escaping a DN component and then concatenating it into a DN path used for bind or lookup, instead of searching by attribute and using the DN the directory returns - escaping reduces risk, but search-then-bind removes the injection point entirely.
  • Treating escaping as a substitute for least privilege: Relying entirely on escaping being correct everywhere while leaving the LDAP bind account with broad read/write access - any gap in escaping (a missed field, a later refactor) is then immediately exploitable at full account privilege.

Language-Specific Guidance

  • C# - DirectorySearcher with allowlist validation and RFC 4515/4514 escaping
  • Java - javax.naming parameterized filter search, LdapName for DNs
  • JavaScript/Node.js - ldapts with filter and DN escaping
  • Python - ldap3 escape_filter_chars and escape_rdn

Additional Resources