LDAP Injection Vulnerabilities

High Risk Input Validation
ldapdirectory-servicesinjectionauthenticationactive-directoryauthorization

What it is

LDAP injection occurs when untrusted user input is used to construct LDAP queries without proper validation or escaping. Attackers can manipulate LDAP queries to bypass authentication, access unauthorized data, or perform directory traversal attacks in directory services like Active Directory, OpenLDAP, and other LDAP-based systems.

import javax.naming.*;
import javax.naming.directory.*;
import java.util.Hashtable;

public class VulnerableLdapAuth {
    private DirContext ldapContext;
    
    public boolean authenticateUser(String username, String password) {
        try {
            // VULNERABLE: Direct string concatenation
            String filter = "(&(uid=" + username + ")(userPassword=" + password + "))";
            
            SearchControls controls = new SearchControls();
            controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
            
            NamingEnumeration<SearchResult> results = ldapContext.search(
                "dc=example,dc=com", filter, controls);
            
            return results.hasMore();
        } catch (Exception e) {
            return false;
        }
    }
}

// Malicious input examples:
// username: "*)(uid=admin))(|(uid=*"
// password: "anything"
// Results in bypassing authentication
import javax.naming.*;
import javax.naming.directory.*;
import java.util.Hashtable;
import java.util.regex.Pattern;

public class SecureLdapAuth {
    private DirContext ldapContext;
    private static final Pattern USERNAME_PATTERN = Pattern.compile("^[a-zA-Z0-9._-]+$");
    
    public boolean authenticateUser(String username, String password) {
        try {
            // Input validation
            if (!isValidInput(username) || !isValidInput(password)) {
                return false;
            }
            
            // SECURE: Proper LDAP escaping
            String escapedUsername = escapeLdapFilter(username);
            String escapedPassword = escapeLdapFilter(password);
            
            String filter = "(&(uid=" + escapedUsername + ")(userPassword=" + escapedPassword + "))";
            
            SearchControls controls = new SearchControls();
            controls.setSearchScope(SearchControls.ONELEVEL_SCOPE); // Restricted scope
            controls.setReturningAttributes(new String[]{"uid"}); // Minimal attributes
            
            NamingEnumeration<SearchResult> results = ldapContext.search(
                "ou=users,dc=example,dc=com", filter, controls);
            
            return results.hasMore();
        } catch (Exception e) {
            // Log the attempt for security monitoring
            System.err.println("LDAP authentication error for user: " + username);
            return false;
        }
    }
    
    private boolean isValidInput(String input) {
        if (input == null || input.length() == 0 || input.length() > 64) {
            return false;
        }
        return USERNAME_PATTERN.matcher(input).matches();
    }
    
    private String escapeLdapFilter(String input) {
        StringBuilder sb = new StringBuilder();
        for (char c : input.toCharArray()) {
            switch (c) {
                case '\\': sb.append("\\5c"); break;
                case '*': sb.append("\\2a"); break;
                case '(': sb.append("\\28"); break;
                case ')': sb.append("\\29"); break;
                case '\u0000': sb.append("\\00"); break;
                default: sb.append(c); break;
            }
        }
        return sb.toString();
    }
}

💡 Why This Fix Works

The vulnerable code directly concatenates user input into LDAP filters, allowing attackers to manipulate query logic. The secure version implements proper input validation and LDAP escaping.

import ldap

class VulnerableLdapSearch:
    def __init__(self, server_url):
        self.conn = ldap.initialize(server_url)
        # VULNERABLE: Using admin credentials
        self.conn.simple_bind_s('cn=admin,dc=example,dc=com', 'admin_pass')
    
    def search_users(self, search_term):
        # VULNERABLE: No input validation or escaping
        ldap_filter = f"(&(objectClass=person)(|(cn=*{search_term}*)(mail=*{search_term}*)))"
        
        try:
            result = self.conn.search_s(
                'dc=example,dc=com',
                ldap.SCOPE_SUBTREE,
                ldap_filter
            )
            return result
        except Exception as e:
            return []

# Malicious input:
# search_term = "*)(objectClass=*))(&(password=*"
# Allows accessing all directory objects
import ldap
from ldap.filter import escape_filter_chars
import logging
import re

class SecureLdapSearch:
    def __init__(self, server_url):
        self.conn = ldap.initialize(server_url)
        # SECURE: Using limited service account
        self.conn.simple_bind_s('cn=app-service,ou=services,dc=example,dc=com', 'service_pass')
        self.logger = logging.getLogger(__name__)
        
    def search_users(self, search_term, max_results=50):
        # Input validation
        if not self._is_valid_search_term(search_term):
            self.logger.warning(f"Invalid search term rejected: {search_term}")
            return []
        
        # SECURE: Proper LDAP filter escaping
        escaped_term = escape_filter_chars(search_term)
        
        ldap_filter = f"(&(objectClass=person)(|(cn=*{escaped_term}*)(mail=*{escaped_term}*)))"
        
        # Log the search for monitoring
        self.logger.info(f"LDAP search: {ldap_filter}")
        
        try:
            # Restricted search with limited scope and attributes
            result = self.conn.search_s(
                'ou=users,dc=example,dc=com',  # Restricted base DN
                ldap.SCOPE_ONELEVEL,           # Limited scope
                ldap_filter,
                ['cn', 'mail', 'uid']          # Only necessary attributes
            )
            
            # Limit results to prevent resource exhaustion
            return result[:max_results]
            
        except ldap.LDAPError as e:
            self.logger.error(f"LDAP search error: {e}")
            return []
    
    def _is_valid_search_term(self, term):
        # Length validation
        if not term or len(term) < 2 or len(term) > 100:
            return False
            
        # Character validation - allow only safe characters
        if not re.match(r'^[a-zA-Z0-9@._\s-]+$', term):
            return False
            
        # Check for obvious injection attempts
        suspicious_patterns = ['*)(', '|(', '))', '\\*', 'objectClass']
        term_lower = term.lower()
        if any(pattern in term_lower for pattern in suspicious_patterns):
            return False
            
        return True
    
    def __del__(self):
        try:
            self.conn.unbind()
        except:
            pass

💡 Why This Fix Works

The vulnerable version uses admin credentials and has no input validation. The secure version implements proper escaping, input validation, restricted search scope, and security logging.

using System;
using System.DirectoryServices;

public class VulnerableAdAuth
{
    private string domain = "LDAP://dc.example.com";
    
    public bool AuthenticateUser(string username, string password)
    {
        try
        {
            // VULNERABLE: Direct string concatenation in LDAP path
            string ldapPath = $"{domain}/CN={username},OU=Users,DC=example,DC=com";
            
            DirectoryEntry entry = new DirectoryEntry(ldapPath, username, password);
            DirectorySearcher searcher = new DirectorySearcher(entry);
            
            // VULNERABLE: Unescaped filter
            searcher.Filter = $"(&(objectClass=user)(sAMAccountName={username}))";
            
            SearchResult result = searcher.FindOne();
            return result != null;
        }
        catch
        {
            return false;
        }
    }
}

// Malicious input:
// username = "*)(objectClass=*))(|(sAMAccountName=admin"
// Bypasses authentication logic
using System;
using System.DirectoryServices;
using System.Text.RegularExpressions;
using System.Text;

public class SecureAdAuth
{
    private string domain = "LDAP://dc.example.com";
    private static readonly Regex usernamePattern = new Regex(@"^[a-zA-Z0-9._-]+$");
    
    public bool AuthenticateUser(string username, string password)
    {
        try
        {
            // Input validation
            if (!IsValidUsername(username) || string.IsNullOrEmpty(password))
            {
                return false;
            }
            
            // SECURE: Using proper DirectoryEntry constructor
            DirectoryEntry entry = new DirectoryEntry(domain, username, password);
            
            // Verify the credentials by attempting to bind
            object nativeObject = entry.NativeObject;
            
            // Additional verification with escaped search
            DirectorySearcher searcher = new DirectorySearcher(entry);
            searcher.SearchRoot = new DirectoryEntry($"{domain}/OU=Users,DC=example,DC=com");
            
            // SECURE: Properly escaped LDAP filter
            string escapedUsername = EscapeLdapFilter(username);
            searcher.Filter = $"(&(objectClass=user)(sAMAccountName={escapedUsername}))";
            searcher.PropertiesToLoad.Add("sAMAccountName");
            
            SearchResult result = searcher.FindOne();
            return result != null;
        }
        catch (Exception ex)
        {
            // Log authentication attempts for monitoring
            Console.WriteLine($"Authentication failed for user {username}: {ex.Message}");
            return false;
        }
    }
    
    private bool IsValidUsername(string username)
    {
        if (string.IsNullOrEmpty(username) || username.Length > 64)
            return false;
            
        return usernamePattern.IsMatch(username);
    }
    
    private string EscapeLdapFilter(string input)
    {
        StringBuilder sb = new StringBuilder();
        foreach (char c in input)
        {
            switch (c)
            {
                case '\\':
                    sb.Append("\\5c");
                    break;
                case '*':
                    sb.Append("\\2a");
                    break;
                case '(':
                    sb.Append("\\28");
                    break;
                case ')':
                    sb.Append("\\29");
                    break;
                case '\0':
                    sb.Append("\\00");
                    break;
                default:
                    sb.Append(c);
                    break;
            }
        }
        return sb.ToString();
    }
}

💡 Why This Fix Works

The vulnerable C# code directly concatenates user input into LDAP paths and filters. The secure version implements proper input validation, LDAP escaping, and uses secure DirectoryEntry authentication.

# VULNERABLE: OpenLDAP configuration
# /etc/openldap/slapd.conf

# No access controls
access to * by * write

# No size or time limits
sizelimit unlimited
timelimit unlimited

# Allowing anonymous binds
allow bind_v2
allow anonymous

# No logging
loglevel 0
# SECURE: OpenLDAP configuration
# /etc/openldap/slapd.conf

# Proper access controls with least privilege
access to attrs=userPassword
    by self write
    by anonymous auth
    by * none

access to dn.subtree="ou=users,dc=example,dc=com"
    by dn="cn=app-service,ou=services,dc=example,dc=com" read
    by users read
    by * none

access to *
    by self read
    by * none

# Set reasonable limits
sizelimit 1000
timelimit 30

# Disable anonymous access
disallow bind_anon
disallow bind_simple_unprotected

# Enable comprehensive logging
loglevel stats filter config parse

# Require TLS for connections
security tls=1

# Account lockout after failed attempts
overlay ppolicy
ppolicy_default "cn=default,ou=policies,dc=example,dc=com"

# Rate limiting
overlay unique
unique_attributes mail uid

💡 Why This Fix Works

LDAP server configuration is crucial for preventing injection attacks. Implement access controls, connection limits, disable anonymous access, and enable security logging.

Why it happens

The most common cause of LDAP injection is directly concatenating user input into LDAP search filters without proper escaping. LDAP uses special characters like parentheses, asterisks, and backslashes that must be escaped to prevent manipulation of the query structure.

Root causes

Unescaped User Input in LDAP Filters

The most common cause of LDAP injection is directly concatenating user input into LDAP search filters without proper escaping. LDAP uses special characters like parentheses, asterisks, and backslashes that must be escaped to prevent manipulation of the query structure.

Preview example – JAVA
// Vulnerable LDAP filter construction
String filter = "(&(uid=" + username + ")(userPassword=" + password + "))";
// Attacker input: username = "*)(uid=*))(|(uid=*"
// Results in: (&(uid=*)(uid=*))(|(uid=*)(userPassword=pass))

Missing Input Validation for LDAP Queries

Applications often fail to validate user input before using it in LDAP queries. This includes not checking for LDAP metacharacters, failing to validate input length, and not implementing whitelist validation for expected input formats.

Preview example – PYTHON
# Vulnerable: No validation before LDAP query
def authenticate_user(username, password):
    ldap_filter = f"(&(cn={username})(userPassword={password}))"
    # Any input accepted, including LDAP metacharacters

Dynamic LDAP Query Construction

Building LDAP queries dynamically based on user preferences or search criteria without proper sanitization creates vulnerabilities. This includes constructing complex filters based on form inputs or API parameters.

Preview example – JAVASCRIPT
// Vulnerable dynamic LDAP filter building
function buildLdapFilter(searchCriteria) {
    let filter = "(&(objectClass=user)";
    for (let field in searchCriteria) {
        filter += `(${field}=${searchCriteria[field]})`;
    }
    filter += ")";
    return filter;
}

Insufficient Access Controls in Directory Services

Using LDAP connections with excessive privileges amplifies the impact of injection attacks. When applications bind to LDAP with administrative privileges, successful injection can lead to unauthorized data access or directory modification.

Preview example – PYTHON
# Dangerous: Using admin credentials for app connections
ldap_connection = ldap.initialize('ldap://dc.example.com')
ldap_connection.simple_bind_s('cn=admin,dc=example,dc=com', 'admin_password')
# Full directory access for injection attacks

Fixes

1

Implement Proper LDAP Filter Escaping

Always escape user input before including it in LDAP filters. Escape LDAP special characters including parentheses, asterisks, backslashes, and null bytes. Use established libraries that provide LDAP escaping functions rather than implementing your own.

View implementation – PYTHON
import ldap
from ldap.filter import escape_filter_chars

def authenticate_user_safe(username, password):
    # Proper LDAP filter escaping
    escaped_username = escape_filter_chars(username)
    escaped_password = escape_filter_chars(password)
    
    ldap_filter = f"(&(cn={escaped_username})(userPassword={escaped_password}))"
    return search_ldap(ldap_filter)
2

Use Parameterized LDAP Queries

When available, use parameterized LDAP query methods that separate query structure from data. Some LDAP libraries provide prepared statement equivalents that prevent injection attacks by treating user input as data rather than query components.

View implementation – JAVA
// Java LDAP with proper parameter binding
LdapContext ctx = new InitialLdapContext(env, null);
SearchControls controls = new SearchControls();

// Use placeholder and parameter array
String filter = "(&(uid={0})(mail={1}))";
Object[] params = {username, email};

NamingEnumeration<SearchResult> results = ctx.search(
    "ou=users,dc=example,dc=com",
    MessageFormat.format(filter, params),
    controls
);
3

Implement Comprehensive Input Validation

Establish robust input validation specifically for LDAP queries. Use whitelist validation for expected characters, implement length limits, validate input formats, and check for LDAP metacharacters before processing.

View implementation – PYTHON
import re

def validate_ldap_input(user_input, field_type='username'):
    # Length validation
    if len(user_input) > 64:  # Reasonable limit
        raise ValueError(f"{field_type} too long")
    
    # Character whitelist validation
    if field_type == 'username':
        if not re.match(r'^[a-zA-Z0-9._-]+$', user_input):
            raise ValueError("Invalid characters in username")
    
    # Check for LDAP injection patterns
    ldap_metacharacters = ['*', '(', ')', '\\', '/', '\x00']
    if any(char in user_input for char in ldap_metacharacters):
        raise ValueError("Invalid characters detected")
    
    return user_input
4

Use Least Privilege LDAP Connections

Configure LDAP connections with minimal necessary permissions. Create dedicated service accounts for application authentication, restrict search base and scope, and avoid using administrative accounts for regular application operations.

View implementation – PYTHON
# Create limited service account
# In LDAP directory:
# cn=app-service,ou=services,dc=example,dc=com
# - Read access only to ou=users
# - Cannot modify directory structure
# - Limited search scope

ldap_connection = ldap.initialize('ldap://dc.example.com')
ldap_connection.simple_bind_s(
    'cn=app-service,ou=services,dc=example,dc=com',
    'service_account_password'
)

# Restrict search base
base_dn = 'ou=users,dc=example,dc=com'
scope = ldap.SCOPE_ONELEVEL  # Not SCOPE_SUBTREE
5

Implement LDAP Query Monitoring and Logging

Monitor LDAP queries for suspicious patterns, log authentication attempts, and implement rate limiting for LDAP operations. Use security tools to detect potential injection attempts and implement alerting for unusual query patterns.

View implementation – PYTHON
import logging
import time
from collections import defaultdict

class LdapSecurityMonitor:
    def __init__(self):
        self.query_counts = defaultdict(int)
        self.last_reset = time.time()
    
    def validate_query(self, username, ldap_filter):
        # Log all LDAP queries
        logging.info(f"LDAP query for user {username}: {ldap_filter}")
        
        # Rate limiting
        self.query_counts[username] += 1
        if self.query_counts[username] > 10:  # 10 queries per minute
            logging.warning(f"Rate limit exceeded for user {username}")
            raise ValueError("Too many authentication attempts")
        
        # Detect suspicious patterns
        suspicious_patterns = ['*)(', '|(', '))', '\\*']
        if any(pattern in ldap_filter for pattern in suspicious_patterns):
            logging.critical(f"Potential LDAP injection from {username}: {ldap_filter}")
            raise ValueError("Suspicious query pattern detected")

Detect This Vulnerability in Your Code

Sourcery automatically identifies ldap injection vulnerabilities and many other security issues in your codebase.