Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions java/ql/lib/change-notes/2026-06-18-ldap-bind-dn-sinks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
category: minorAnalysis
---
* Added LDAP bind-DN sinks to the `java/ldap-injection` query: the `String name` argument of `javax.naming.Context` / `javax.naming.directory.DirContext` `bind`, `rebind`, `lookup`, `lookupLink`, and `createSubcontext`; the `java.naming.security.principal` JNDI environment value; and the `principal` argument of Apache Shiro `LdapContextFactory.getLdapContext`. The query now detects LDAP distinguished-name injection (CWE-90) into a bind DN, not just into a search filter or search base. `new javax.naming.ldap.LdapName(String)` is deliberately not modelled as a sink, as it commonly parses an existing certificate or principal DN rather than constructing one for a bind.
7 changes: 7 additions & 0 deletions java/ql/lib/ext/javax.naming.directory.model.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,10 @@ extensions:
extensible: sinkModel
data:
- ["javax.naming.directory", "DirContext", True, "search", "", "", "Argument[0..1]", "ldap-injection", "manual"]
# The `String name` argument is interpreted as a (distinguished) name; an
# unescaped, attacker-controlled value lets the caller manipulate the bind DN
# (LDAP DN injection, CWE-90). Only the `(String,...)` overloads matter -- the
# `(Name,...)` overloads take a structured, already-parsed name.
- ["javax.naming.directory", "DirContext", True, "bind", "(String,Object,Attributes)", "", "Argument[0]", "ldap-injection", "manual"]
- ["javax.naming.directory", "DirContext", True, "rebind", "(String,Object,Attributes)", "", "Argument[0]", "ldap-injection", "manual"]
- ["javax.naming.directory", "DirContext", True, "createSubcontext", "(String,Attributes)", "", "Argument[0]", "ldap-injection", "manual"]
11 changes: 11 additions & 0 deletions java/ql/lib/ext/javax.naming.model.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@ extensions:
- ["javax.naming", "Context", True, "lookupLink", "", "", "Argument[0]", "jndi-injection", "manual"]
- ["javax.naming", "Context", True, "rename", "", "", "Argument[0]", "jndi-injection", "manual"]
- ["javax.naming", "InitialContext", True, "doLookup", "", "", "Argument[0]", "jndi-injection", "manual"]
# The `String name` argument of these methods is interpreted as a (distinguished)
# name; an unescaped, attacker-controlled value lets the caller manipulate the
# bind DN (LDAP DN injection, CWE-90). `bind`/`rebind`/`createSubcontext` create
# an entry at the given DN; `lookup`/`lookupLink` resolve it (e.g. to authenticate
# a bind DN). Only the `(String,...)` overloads matter -- the `(Name,...)`
# overloads take a structured, already-parsed name.
- ["javax.naming", "Context", True, "bind", "(String,Object)", "", "Argument[0]", "ldap-injection", "manual"]
- ["javax.naming", "Context", True, "rebind", "(String,Object)", "", "Argument[0]", "ldap-injection", "manual"]
- ["javax.naming", "Context", True, "createSubcontext", "(String)", "", "Argument[0]", "ldap-injection", "manual"]
- ["javax.naming", "Context", True, "lookup", "(String)", "", "Argument[0]", "ldap-injection", "manual"]
- ["javax.naming", "Context", True, "lookupLink", "(String)", "", "Argument[0]", "ldap-injection", "manual"]

- addsTo:
pack: codeql/java-all
Expand Down
12 changes: 12 additions & 0 deletions java/ql/lib/ext/org.apache.shiro.realm.ldap.model.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
extensions:
- addsTo:
pack: codeql/java-all
extensible: sinkModel
data:
# `LdapContextFactory.getLdapContext(principal, credentials)` binds to the
# directory using `principal` as the bind DN. An unescaped, attacker-controlled
# principal lets the caller manipulate the DN structure (LDAP DN injection,
# CWE-90). This is the sink in Apache Shiro CVE-2026-49268, where
# `DefaultLdapRealm.getUserDn` / `ActiveDirectoryRealm.getUsernameWithSuffix`
# concatenated the login username into the bind DN with no `Rdn.escapeValue`.
- ["org.apache.shiro.realm.ldap", "LdapContextFactory", True, "getLdapContext", "(Object,Object)", "", "Argument[0]", "ldap-injection", "manual"]
33 changes: 33 additions & 0 deletions java/ql/lib/semmle/code/java/security/LdapInjection.qll
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,39 @@ private class DefaultLdapInjectionSink extends LdapInjectionSink {
DefaultLdapInjectionSink() { sinkNode(this, "ldap-injection") }
}

/**
* The value of a `java.naming.security.principal` JNDI environment entry, i.e. the
* `value` argument of a `Hashtable`/`Map.put(key, value)` whose key is the
* `javax.naming.Context.SECURITY_PRINCIPAL` constant or the literal string
* `"java.naming.security.principal"`.
*
* This entry is the bind DN used to authenticate the directory connection. An
* unescaped, attacker-controlled value lets the caller manipulate the DN structure
* (LDAP DN injection, CWE-90). This pattern cannot be expressed as a Models-as-Data
* sink because it depends on the value of the `key` argument rather than the method
* signature.
*
* Note: `new javax.naming.ldap.LdapName(String)` is deliberately not modelled as a
* sink. It over-fires on the benign idiom of parsing an existing certificate or
* principal DN to read its RDNs (e.g.
* `new LdapName(cert.getSubjectX500Principal().getName()).getRdns()`), which is not
* injection. The injection sinks are the positions where a DN string is used to bind,
* look up, or authenticate.
*/
private class SecurityPrincipalEnvSink extends LdapInjectionSink {
SecurityPrincipalEnvSink() {
exists(MethodCall ma |
ma.getMethod().hasName("put") and
this.asExpr() = ma.getArgument(1)
|
ma.getArgument(0).(FieldRead).getField().hasName("SECURITY_PRINCIPAL")
or
ma.getArgument(0).(CompileTimeConstantExpr).getStringValue() =
"java.naming.security.principal"
)
}
}

/** A sanitizer that clears the taint on (boxed) primitive types. */
private class DefaultLdapSanitizer extends LdapInjectionSanitizer instanceof SimpleTypeSanitizer { }

Expand Down
Loading
Loading