LDAP injection

LDAP injection

Description

LDAP filter injection occurs when untrusted data is inserted into a search filter without RFC 4515 escaping. An attacker can add filter operators or wildcards to change its conditions, potentially bypassing authentication or retrieving directory entries the application did not intend to expose.

Potential impact

  • User-search or authentication conditions may be bypassed.
  • Sensitive directory information, such as users, groups, and email addresses, may be disclosed.
  • Broad or expensive filters may increase LDAP server load. The actual impact depends on directory access controls and search limits.

Remediation

  • Keep the filter structure and attribute names fixed. Insert untrusted data only as assertion values.
  • With python-ldap, escape each assertion value using ldap.filter.escape_filter_chars, or pass a trusted, fixed template and a separate value list to ldap.filter.filter_format. Do not construct its first argument, the template, from external input.
  • With ldap3, escape each assertion value using ldap3.utils.conv.escape_filter_chars before inserting it into a search filter.
  • Search-filter escaping and DN escaping are different. When building a DN, apply ldap.dn.escape_dn_chars or ldap3.utils.dn.escape_rdn to the relevant RFC 4514 values.
  • Apply business allow-lists and length limits where appropriate, and bind with a least-privileged account. These controls complement context-specific escaping; they do not replace it.

Examples

Before

python
import ldap
from flask import request

def bad_search():
    conn = ldap.initialize("ldaps://directory.example.com")
    username = request.args.get("user")
    query = "(uid=%s)" % username
    return conn.search_s("ou=users,dc=example,dc=com", ldap.SCOPE_SUBTREE, query)

After

python
import ldap
from ldap.filter import filter_format
from flask import request

def safe_search():
    conn = ldap.initialize("ldaps://directory.example.com")
    username = request.args.get("user")
    query = filter_format("(uid=%s)", [username])
    return conn.search_s("ou=users,dc=example,dc=com", ldap.SCOPE_SUBTREE, query)

Explanation:

  • Before: User input is inserted directly into the LDAP filter string.
  • After: The application fixes the filter template, and filter_format applies RFC 4515 escaping to the assertion values supplied separately.

Implementation considerations

Filter escaping does not replace TLS verification, bind-account permissions, or required-input validation. These excerpts focus on constructing the search filter; prepare those settings separately. When building a DN or another LDAP expression, use escaping appropriate to that context.

References