Skip to main content

Overview

LDAP Injection is an attack that exploits applications that construct LDAP (Lightweight Directory Access Protocol) queries from user-supplied input. If an application fails to sanitize this input, an attacker can inject LDAP metacharacters (*, (, ), &, |, etc.) to modify the query. This can lead to bypassing authentication, escalating privileges, or disclosing sensitive information from the directory.

Business Impact

Since LDAP directories are often the central source of truth for user authentication and authorization in an enterprise, a successful LDAP Injection attack can be catastrophic. It can allow an attacker to bypass login controls for critical applications, grant themselves administrative privileges, or exfiltrate the entire corporate user directory.

Reference Details

CWE ID: CWE-90 OWASP Top 10 (2021): A03:2021 - Injection Severity: High

Framework-Specific Analysis and Remediation

The core of LDAP Injection is identical to SQL Injection: mixing untrusted data with code (in this case, the LDAP filter syntax). The universal defense is to always escape or sanitize user-supplied input before it is placed within an LDAP filter. All special characters in the input must be properly escaped so they are treated as literal values, not as operators.
  • Python
  • Java
  • .NET(C#)
  • PHP
  • Node.js
  • Ruby

Framework Context

Django applications typically use the python-ldap library for LDAP integration. This library provides a utility for escaping, but developers often forget to use it when building filters manually.

Vulnerable Scenario 1: User Authentication

A custom authentication backend attempts to bind to an LDAP server by constructing a filter from the username.
# myapp/auth.py
import ldap

def authenticate_user(username, password):
    conn = ldap.initialize('ldap://ldap.example.com')
    # DANGEROUS: The username is directly formatted into the filter.
    # Payload for username: "admin)(uid=*" - this filter becomes "(&(uid=admin)(uid=*)(userPassword=...))"
    # If 'admin' is a valid user, this can bypass the password check.
    search_filter = f"(&(uid={username})(userPassword={password}))"
    try:
        conn.simple_bind_s(f"uid={username},ou=users,dc=example,dc=com", password)
        # A search might be performed here
        return True
    except ldap.INVALID_CREDENTIALS:
        return False

Vulnerable Scenario 2: Employee Search Feature

An internal portal allows searching for employees by their common name (cn).
# employees/views.py
def search_employees(request):
    name = request.GET.get('name')
    # DANGEROUS: An attacker can use a wildcard to dump all users.
    # Payload: "*" which makes the filter "(cn=*)"
    search_filter = f"(cn={name})"
    results = conn.search_s('ou=people,dc=example,dc=com', ldap.SCOPE_SUBTREE, search_filter)
    # ...

Mitigation and Best Practices

Use the ldap.filter.escape_filter_chars() function on all user input that will be part of an LDAP filter. This correctly escapes special characters like *, (, ), \, etc.

Secure Code Example

# myapp/auth.py (Secure Version)
import ldap
import ldap.filter

def authenticate_user(username, password):
    conn = ldap.initialize('ldap://ldap.example.com')
    
    # SAFE: The username is escaped before being used in the filter.
    # The payload "admin)(uid=*" would be transformed into a harmless literal string.
    safe_username = ldap.filter.escape_filter_chars(username)
    search_filter = f"(&(uid={safe_username})(userPassword={password}))"
    
    # For authentication, binding is generally safer than searching.
    try:
        # Note: Storing and checking plain-text passwords in LDAP is a bad practice itself.
        # This is for demonstrating the injection fix.
        conn.simple_bind_s(f"uid={safe_username},ou=users,dc=example,dc=com", password)
        return True
    except ldap.INVALID_CREDENTIALS:
        return False

Testing Strategy

Write unit tests for the authentication/search function. Pass payloads containing LDAP metacharacters (e.g., testuser*, admin)(uid=*, *) and assert that the function behaves as expected (e.g., fails authentication, returns no results) rather than executing the modified filter.
# employees/tests.py
def test_ldap_search_injection(self):
    # Mock the `python-ldap` library's search method
    with patch('ldap.ldapobject.SimpleLDAPObject.search_s') as mock_search:
        search_employees_with_payload("*") # A helper that calls the view logic
        
        # The test should assert that the filter passed to the real search
        # method contains the correctly escaped string, not the raw wildcard.
        called_filter = mock_search.call_args[0][2]
        self.assertEqual(called_filter, "(cn=\\2a)") # `*` is escaped to `\2a`