CWE-90: LDAP Injection - Java
Overview
LDAP Injection occurs when untrusted data is used to construct LDAP queries without proper encoding, allowing attackers to manipulate directory searches and access unauthorized data.
Primary Defence: Use OWASP ESAPI's encodeForLDAP() and encodeForDN() methods, or Spring LDAP's query builder which provides automatic encoding.
Common Vulnerable Patterns
Direct String Concatenation in LDAP Queries
// VULNERABLE - No encoding
import javax.naming.*;
import javax.naming.directory.*;
public User findUser(String username) throws NamingException {
// Dangerous! User input directly in filter
String filter = "(uid=" + username + ")";
DirContext ctx = new InitialDirContext();
SearchControls searchControls = new SearchControls();
NamingEnumeration<SearchResult> results =
ctx.search("ou=users,dc=example,dc=com", filter, searchControls);
return processResults(results);
}
// Attack: username = "*)(uid=*))(|(uid=*"
// Results in: (uid=*)(uid=*))(|(uid=*)
Building DN Paths Without Encoding
// VULNERABLE
public void updateUser(String username, String ou) throws NamingException {
String dn = "cn=" + username + ",ou=" + ou + ",dc=example,dc=com";
DirContext ctx = new InitialDirContext();
ctx.lookup(dn); // Vulnerable to DN injection
}
Secure Patterns
Using ESAPI for LDAP Filter Encoding
// SECURE - Encode filter values
import org.owasp.esapi.ESAPI;
import javax.naming.*;
import javax.naming.directory.*;
public class SecureLdapService {
private final Encoder encoder = ESAPI.encoder();
public User findUser(String username) throws NamingException {
// Encode the username for safe use in LDAP filter
String safeUsername = encoder.encodeForLDAP(username);
String filter = "(uid=" + safeUsername + ")";
DirContext ctx = new InitialDirContext();
SearchControls controls = new SearchControls();
controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
NamingEnumeration<SearchResult> results =
ctx.search("ou=users,dc=example,dc=com", filter, controls);
return processResults(results);
}
// SECURE - Complex filter with multiple parameters
public List<User> searchUsers(String firstName, String lastName, String department)
throws NamingException {
String safeFirst = encoder.encodeForLDAP(firstName);
String safeLast = encoder.encodeForLDAP(lastName);
String safeDept = encoder.encodeForLDAP(department);
String filter = "(&(givenName=" + safeFirst + ")" +
"(sn=" + safeLast + ")" +
"(department=" + safeDept + "))";
DirContext ctx = new InitialDirContext();
SearchControls controls = new SearchControls();
NamingEnumeration<SearchResult> results =
ctx.search("ou=users,dc=example,dc=com", filter, controls);
return processResults(results);
}
}
Why this works:
- OWASP ESAPI's
encodeForLDAP()method escapes all special characters that have syntactic meaning in LDAP filter expressions:*,(,),\, andNUL. - These characters allow attackers to manipulate filter logic if left unescaped.
- For example, injecting
*)(uid=*))(|(uid=*without encoding creates multiple filter clauses that can bypass authentication or extract all directory entries. - ESAPI converts special characters to backslash-hex escape sequences (e.g.,
*becomes\2a,(becomes\28) following RFC 4515, ensuring the LDAP server interprets user input as literal search values rather than filter operators. - This prevents filter injection attacks where attackers craft queries to access unauthorized data, escalate privileges, or cause denial of service.
- ESAPI is the industry-standard solution for LDAP encoding in Java.
Using ESAPI for DN Encoding
// SECURE - Encode DN components
import org.owasp.esapi.ESAPI;
public class SecureLdapService {
private final Encoder encoder = ESAPI.encoder();
public void updateUser(String username, String ou) throws NamingException {
String safeCN = encoder.encodeForDN(username);
String safeOU = encoder.encodeForDN(ou);
String dn = "cn=" + safeCN + ",ou=" + safeOU + ",dc=example,dc=com";
DirContext ctx = new InitialDirContext();
Attributes attrs = ctx.getAttributes(dn);
// ... update attributes
}
public void createUser(String username, String email, String ou)
throws NamingException {
String safeCN = encoder.encodeForDN(username);
String safeOU = encoder.encodeForDN(ou);
String dn = "cn=" + safeCN + ",ou=" + safeOU + ",dc=example,dc=com";
Attributes attrs = new BasicAttributes(true);
attrs.put("cn", safeCN);
attrs.put("mail", email);
attrs.put("objectClass", "inetOrgPerson");
DirContext ctx = new InitialDirContext();
ctx.createSubcontext(dn, attrs);
}
}
Why this works:
- ESAPI's
encodeForDN()method escapes DN-specific special characters including,,+,",\,<,>,;,=, and leading/trailing spaces that have structural meaning in LDAP Distinguished Names. - Without encoding, attackers can inject DN components to traverse the directory tree or access objects in different OUs.
- For example,
username = "admin,OU=Admins,DC=evil,DC=com"breaks out of the intended DN path. - ESAPI escapes these characters (e.g.,
,becomes\,) following RFC 4514, treating them as literal RDN values rather than DN separators. - However, even with encoding, constructing DNs from user input is riskier than using filter searches.
- Best practice is to search by attribute first (with
encodeForLDAP()) and use the DN returned by the directory, which is fully trusted and doesn't require encoding.
Input Validation (Defense in Depth)
// SECURE - Validation + Encoding
import org.owasp.esapi.ESAPI;
import java.util.regex.Pattern;
public class SecureLdapService {
private final Encoder encoder = ESAPI.encoder();
private static final Pattern USERNAME_PATTERN = Pattern.compile("^[a-zA-Z0-9._-]+$");
private static final Pattern DEPT_PATTERN = Pattern.compile("^[a-zA-Z0-9\\s]+$");
public User findUser(String username) throws NamingException {
// Validate input format
if (!USERNAME_PATTERN.matcher(username).matches()) {
throw new IllegalArgumentException("Invalid username format");
}
// Still encode even after validation (defense in depth)
String safeUsername = encoder.encodeForLDAP(username);
String filter = "(uid=" + safeUsername + ")";
DirContext ctx = new InitialDirContext();
SearchControls controls = new SearchControls();
NamingEnumeration<SearchResult> results =
ctx.search("ou=users,dc=example,dc=com", filter, controls);
return processResults(results);
}
public List<User> findByDepartment(String department) throws NamingException {
if (department == null || department.trim().isEmpty()) {
throw new IllegalArgumentException("Department cannot be empty");
}
if (!DEPT_PATTERN.matcher(department).matches()) {
throw new IllegalArgumentException("Invalid department format");
}
String safeDept = encoder.encodeForLDAP(department);
String filter = "(department=" + safeDept + ")";
DirContext ctx = new InitialDirContext();
SearchControls controls = new SearchControls();
NamingEnumeration<SearchResult> results =
ctx.search("ou=users,dc=example,dc=com", filter, controls);
return processResults(results);
}
}
Why this works:
- Input validation with regex patterns provides an early defense layer by rejecting syntactically invalid input before it reaches LDAP operations.
- The allowlist approach (e.g.,
^[a-zA-Z0-9._-]+$for usernames) restricts input to expected character sets, catching obviously malicious payloads that contain unusual characters or injection attempts. - This reduces the attack surface and prevents edge cases or parser bugs.
- However, validation alone is never sufficient for LDAP injection prevention - you must still use ESAPI encoding after validation.
- Validation can have subtle flaws (overly permissive patterns) or might need to allow legitimate special characters in some contexts.
- The combination of validation + encoding provides defense-in-depth: validation rejects clearly invalid input early, while encoding ensures any special characters that pass validation are properly escaped.
- This layered approach is more robust than either technique alone.
Using Prepared-Style Approach with Placeholders
// SECURE - Template-based approach
public class LdapQueryBuilder {
private final Encoder encoder = ESAPI.encoder();
public String buildUserFilter(String username) {
String safe = encoder.encodeForLDAP(username);
return String.format("(uid=%s)", safe);
}
public String buildComplexFilter(Map<String, String> attributes) {
StringBuilder filter = new StringBuilder("(&");
for (Map.Entry<String, String> entry : attributes.entrySet()) {
String safeValue = encoder.encodeForLDAP(entry.getValue());
filter.append("(")
.append(entry.getKey())
.append("=")
.append(safeValue)
.append(")");
}
filter.append(")");
return filter.toString();
}
}
Why this works:
- Template-based filter building separates filter structure from user data, making it easier to ensure all user values are encoded.
- By using methods like
String.format()orStringBuilder, you define the filter syntax (operators, parentheses, attribute names) separately from user-supplied values, reducing the risk of forgetting to encode a value. - Each user value passes through
encodeForLDAP()before insertion into the template. - This pattern is particularly valuable for complex filters with multiple user inputs, where manual string concatenation becomes error-prone.
- The template approach also improves code readability and maintainability - developers can clearly see the filter structure and identify which values need encoding.
- However, the template itself must be constructed correctly to avoid introducing filter syntax errors.
- This pattern works well with builder classes that encapsulate filter construction logic.
Using Spring LDAP Query Builder (Automatic Encoding)
// SECURE - Spring LDAP handles encoding automatically
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.query.LdapQueryBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class SpringLdapService {
@Autowired
private LdapTemplate ldapTemplate;
public User findUser(String username) {
// Spring LDAP automatically encodes filter values
return ldapTemplate.findOne(
LdapQueryBuilder.query()
.where("uid").is(username), // Automatically encoded
User.class
);
}
public List<User> searchUsers(String firstName, String lastName, String department) {
// Complex filters with automatic encoding
return ldapTemplate.find(
LdapQueryBuilder.query()
.where("givenName").is(firstName)
.and("sn").is(lastName)
.and("department").is(department),
User.class
);
}
public List<User> findByDepartmentOrRole(String department, String role) {
// OR logic with automatic encoding
return ldapTemplate.find(
LdapQueryBuilder.query()
.where("department").is(department)
.or("role").is(role),
User.class
);
}
// Configuration
@Bean
public LdapTemplate ldapTemplate(ContextSource contextSource) {
return new LdapTemplate(contextSource);
}
}
// Maven dependency:
// <dependency>
// <groupId>org.springframework.ldap</groupId>
// <artifactId>spring-ldap-core</artifactId>
// </dependency>
Why this works:
- Spring LDAP's
LdapQueryBuilderprovides automatic LDAP filter encoding, eliminating the need for manual escaping. - When you use
.where("uid").is(username), Spring internally calls LDAP encoding on theusernamevalue before constructing the filter string. - This abstraction prevents developers from forgetting to encode values and reduces code verbosity.
- The query builder pattern also makes LDAP queries more readable and type-safe compared to manual string concatenation.
- Spring LDAP handles all RFC 4515 special characters (
*,(,),\,NUL) automatically. - However, this only works when using the query builder API - if you manually construct filter strings and pass them to
ldapTemplate.search(baseDn, filterString, mapper), you're responsible for encoding. - Always use the builder methods (
.where(),.and(),.or()) to benefit from automatic encoding. - This is the recommended approach for Spring applications as it combines safety with developer productivity.
Common Attack Vectors
LDAP Filter Injection
// Attack: username = "*)(uid=*))(|(uid=*"
// Without encoding: (uid=*)(uid=*))(|(uid=*)
// This can bypass authentication or extract all users
// With ESAPI encoding:
String encoded = encoder.encodeForLDAP("*)(uid=*))(|(uid=*");
// Results in: \2a\29\28uid=\2a\29\29\28|\28uid=\2a
// Special characters are properly escaped
DN Injection
// Attack: ou = "Admins,DC=evil,DC=com"
// Without encoding: CN=user,OU=Admins,DC=evil,DC=com
// Could access wrong directory tree
// With ESAPI encoding:
String encoded = encoder.encodeForDN("Admins,DC=evil,DC=com");
// Results in: Admins\,DC=evil\,DC=com
// Commas are escaped, preventing DN manipulation
Verification
To verify LDAP injection protection:
- Test with injection payloads: Submit LDAP filter injection attempts (e.g.,
*)(uid=*))(|(uid=*) and verify they're properly encoded - Check encoded output: Verify special characters are escaped (e.g.,
*becomes\2a,(becomes\28,)becomes\29) - Test DN encoding: Attempt DN injection with commas and verify they're escaped (
,becomes\,) - Review LDAP queries: Search codebase for LDAP search operations and ensure all user input is encoded with
encodeForLDAP()orencodeForDN() - Test authentication bypass: Attempt to bypass authentication with wildcard filters and verify they fail
- Verify framework usage: If using Spring LDAP, confirm the query builder is used instead of manual filter construction
- Test with null bytes: Submit
\00(null byte) and verify it's properly escaped - Check for
encodeForLDAPvsencodeForDN: Ensure the correct encoding method is used for each context (filters vs DNs)