LDAP injection
Need
Authenticated and encrypted binds for every LDAP connection
Context
• Usage of Kotlin 1.9+ on the JVM for building application services
• Usage of JNDI (javax.naming) for LDAP directory access
Description
1. Non compliant code
import java.util.Hashtable
import javax.naming.Context
import javax.naming.directory.InitialDirContext
fun openDirectory(): InitialDirContext {
val env = Hashtable<String, String>()
env[Context.INITIAL_CONTEXT_FACTORY] = "com.sun.jndi.ldap.LdapCtxFactory"
env[Context.PROVIDER_URL] = "ldap://ldap.example.com:389"...The `openDirectory` function below connects to the LDAP server with `Context.SECURITY_AUTHENTICATION` set to `"none"`, an anonymous bind, over plain `ldap://`. An anonymous bind means the directory cannot tell who is querying it, so its access control can only allow or deny everyone. When anonymous reads are enabled, any code that reaches this connection, including queries built from user input, can read the directory tree: user names, email addresses, group memberships and sometimes password attributes. Combined with an LDAP filter built from request data, the attacker can enumerate every entry with values such as `*`. The plain `ldap://` URL also sends every query and result in clear text.
2. Steps
• Never set `Context.SECURITY_AUTHENTICATION` to `"none"`; bind with `"simple"` or a stronger mechanism as a dedicated service account.
• Load the bind credentials at runtime from a secrets manager instead of the source code.
• Connect with `ldaps://` or StartTLS so credentials and results are encrypted.
• Pass user input to searches only through the `filterArgs` parameter of `DirContext.search`, which escapes filter metacharacters.
• Disable anonymous reads on the directory server and grant the service account only the attributes it needs.
3. Secure code example
import java.util.Hashtable
import javax.naming.Context
import javax.naming.directory.InitialDirContext
fun openDirectory(): InitialDirContext {
val env = Hashtable<String, String>()
env[Context.INITIAL_CONTEXT_FACTORY] = "com.sun.jndi.ldap.LdapCtxFactory"
env[Context.PROVIDER_URL] = "ldaps://ldap.example.com:636"...The corrected function binds with `"simple"` authentication as a dedicated service account, whose distinguished name and password are read at runtime from `LDAP_BIND_DN` and `LDAP_BIND_PASSWORD`. The directory can then apply its access control to that account and grant it only the attributes the application needs. The connection uses `ldaps://` on port 636, so the credentials and every query travel inside TLS and the server certificate is verified against the trust store of the JVM. Filters built from request data must still be escaped: every value should go through the parameter array of `DirContext.search(name, filterExpr, filterArgs, controls)`, which escapes it, instead of being concatenated into the filter string.
References
• 107. LDAP injection