From ea48dfeaad64bda6ed70eedc25544e44106bfcb3 Mon Sep 17 00:00:00 2001 From: tonghuaroot Date: Thu, 18 Jun 2026 10:32:07 +0800 Subject: [PATCH] Java: add experimental java/ldap-dn-injection-library-mode query The supported java/ldap-injection query starts from remote flow sources, so it does not report on authentication frameworks, where the login principal arrives as a method parameter rather than at a servlet parameter or similar. This experimental query detects LDAP distinguished-name injection (CWE-90, RFC 2253) into a bind DN inside such a framework. Sources are library-boundary values: login-principal accessors of common auth frameworks (Apache Shiro AuthenticationToken, Spring Security Authentication, java.security.Principal) and the string parameters of DN-builder-shaped methods. The DN-builder source model is name-heuristic, a deliberate precision/recall trade for the library case where there is no remote flow source to anchor on; the query is therefore experimental and medium precision. Sinks are the bind-DN positions: javax.naming Context / DirContext bind, rebind, lookup, lookupLink, createSubcontext; the java.naming.security.principal environment value; and Apache Shiro LdapContextFactory.getLdapContext. Barriers are RFC 2253 DN escapers such as Rdn.escapeValue. Anchored on Apache Shiro CVE-2026-49268. Adds a qhelp, a true-positive/true-negative test, and the Shiro stubs the test needs. --- .../CWE-090/LdapDnInjectionLibraryMode.qhelp | 79 ++++++++ .../CWE/CWE-090/LdapDnInjectionLibraryMode.ql | 180 ++++++++++++++++++ .../LdapDnInjectionLibraryModeBad.java | 21 ++ .../LdapDnInjectionLibraryModeGood.java | 22 +++ .../LdapDnInjectionLibraryMode.expected | 75 ++++++++ .../CWE-090/LdapDnInjectionLibraryMode.java | 92 +++++++++ .../CWE-090/LdapDnInjectionLibraryMode.qlref | 4 + .../query-tests/security/CWE-090/options | 1 + .../shiro/authc/UsernamePasswordToken.java | 15 ++ .../shiro/realm/ldap/LdapContextFactory.java | 13 ++ 10 files changed, 502 insertions(+) create mode 100644 java/ql/src/experimental/Security/CWE/CWE-090/LdapDnInjectionLibraryMode.qhelp create mode 100644 java/ql/src/experimental/Security/CWE/CWE-090/LdapDnInjectionLibraryMode.ql create mode 100644 java/ql/src/experimental/Security/CWE/CWE-090/LdapDnInjectionLibraryModeBad.java create mode 100644 java/ql/src/experimental/Security/CWE/CWE-090/LdapDnInjectionLibraryModeGood.java create mode 100644 java/ql/test/experimental/query-tests/security/CWE-090/LdapDnInjectionLibraryMode.expected create mode 100644 java/ql/test/experimental/query-tests/security/CWE-090/LdapDnInjectionLibraryMode.java create mode 100644 java/ql/test/experimental/query-tests/security/CWE-090/LdapDnInjectionLibraryMode.qlref create mode 100644 java/ql/test/experimental/query-tests/security/CWE-090/options create mode 100644 java/ql/test/experimental/stubs/org-apache-shiro-authc-2.0.1/org/apache/shiro/authc/UsernamePasswordToken.java create mode 100644 java/ql/test/experimental/stubs/org-apache-shiro-realm-ldap-2.0.1/org/apache/shiro/realm/ldap/LdapContextFactory.java diff --git a/java/ql/src/experimental/Security/CWE/CWE-090/LdapDnInjectionLibraryMode.qhelp b/java/ql/src/experimental/Security/CWE/CWE-090/LdapDnInjectionLibraryMode.qhelp new file mode 100644 index 000000000000..6cfe30e66006 --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-090/LdapDnInjectionLibraryMode.qhelp @@ -0,0 +1,79 @@ + + + + +

+An LDAP distinguished name (DN) identifies an entry in a directory, for example +uid=alice,ou=people,dc=example,dc=com. When an authentication framework +builds the bind DN by concatenating the login principal into a DN template without +escaping it for RFC 2253, an attacker can supply DN metacharacters +(, + " \ < > ; =, a leading #, or leading/trailing +whitespace) to change the structure of the DN that is used to authenticate. Depending +on the directory, this can bypass authentication or impersonate another principal. +

+

+This query targets the defect inside an authentication library or framework +(Apache Shiro, a custom Spring Security realm, a CAS or pac4j SPI, a Keycloak provider), +where the login principal does not arrive at a remote flow source such as a servlet +parameter, but as a method parameter at the library boundary. The supported +java/ldap-injection query, which starts from remote flow sources, does not +report on such a framework because there is no remote flow source to start from. +

+

+The DN escape set (RFC 2253) differs from the LDAP search-filter escape set (RFC 4515). +A value escaped for a search filter (for example with LdapEncoder.filterEncode) +is still unsafe in a DN, and vice versa. This query treats only DN escapers as +sanitizers. +

+

+The library-mode source model is name-heuristic: it treats the login-principal +accessors of common authentication frameworks, and the string parameters of +DN-builder-shaped methods (for example getUserDn or +getUsernameWithSuffix), as sources. This is a deliberate +precision/recall trade for the library case, where there is no remote flow source to +anchor on. A framework that builds the DN in a differently named helper is missed, and +a benign method that matches the name pattern may produce a false positive; this is why +the query is experimental and uses medium precision. Triage a result by confirming the +value reaches a real bind sink unescaped. +

+
+ + +

+Escape the login principal for RFC 2253 before placing it in a DN, for example with +javax.naming.ldap.Rdn.escapeValue, Spring LDAP +LdapEncoder.nameEncode, or OWASP ESAPI encodeForDN. Prefer +building the DN from structured components (an LdapName and +Rdn objects) rather than string concatenation. +

+
+ + +

+The following example concatenates the login principal into the bind DN with no +escaping. An attacker who logs in as * or +admin,ou=admins,dc=example,dc=com+uid=anything can manipulate the DN. +

+ +

+The following example escapes the principal with Rdn.escapeValue before +building the DN, so DN metacharacters are neutralised. +

+ +
+ + +
  • +OWASP: LDAP Injection. +
  • +
  • +RFC 2253: UTF-8 String Representation of Distinguished Names. +
  • +
  • +Java SE API: Rdn.escapeValue. +
  • +
    + +
    diff --git a/java/ql/src/experimental/Security/CWE/CWE-090/LdapDnInjectionLibraryMode.ql b/java/ql/src/experimental/Security/CWE/CWE-090/LdapDnInjectionLibraryMode.ql new file mode 100644 index 000000000000..131184f87712 --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-090/LdapDnInjectionLibraryMode.ql @@ -0,0 +1,180 @@ +/** + * @name LDAP distinguished name injection in authentication framework code (library-mode sources) + * @description Building an LDAP bind distinguished name (DN) from an unescaped login + * principal lets an attacker manipulate the DN structure used to + * authenticate, potentially bypassing authentication or impersonating + * another principal. This variant uses library-boundary sources to find + * the defect inside an authentication framework, where the principal + * arrives as a method parameter rather than at a remote flow source. + * @kind path-problem + * @problem.severity error + * @security-severity 8.1 + * @precision medium + * @id java/ldap-dn-injection-library-mode + * @tags security + * experimental + * external/cwe/cwe-090 + */ + +import java +import semmle.code.java.dataflow.TaintTracking +import LdapDnLibraryModeFlow::PathGraph + +/** + * The `String name` argument of `javax.naming.Context` / `DirContext` + * `bind` / `rebind` / `lookup` / `lookupLink` / `createSubcontext` -- interpreted as + * a (composite or distinguished) name when given a `String`. + * + * `new javax.naming.ldap.LdapName(String)` is deliberately not used as a sink: it + * commonly parses 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. + */ +class JndiNameLookupMethod extends Method { + JndiNameLookupMethod() { + this.hasName(["bind", "rebind", "lookup", "lookupLink", "createSubcontext"]) and + this.getDeclaringType() + .getAnAncestor*() + .hasQualifiedName("javax.naming", ["Context", "directory.DirContext"]) and + this.getParameterType(0) instanceof TypeString + } +} + +/** + * A call to `Map.put` / `Hashtable.put` whose key is the + * `javax.naming.Context.SECURITY_PRINCIPAL` constant or the literal string + * `"java.naming.security.principal"`. The value argument is the bind DN. + */ +class SecurityPrincipalPut extends MethodCall { + SecurityPrincipalPut() { + this.getMethod().hasName("put") and + ( + this.getArgument(0).(FieldRead).getField().hasName("SECURITY_PRINCIPAL") + or + this.getArgument(0).(CompileTimeConstantExpr).getStringValue() = + "java.naming.security.principal" + ) + } +} + +/** + * The `principal` argument of Apache Shiro + * `LdapContextFactory.getLdapContext(principal, credentials)` -- the bind DN. This is + * the sink in Apache Shiro CVE-2026-49268. + */ +class ShiroGetLdapContextMethod extends Method { + ShiroGetLdapContextMethod() { + this.hasName("getLdapContext") and + this.getDeclaringType() + .getAnAncestor*() + .hasQualifiedName("org.apache.shiro.realm.ldap", "LdapContextFactory") + } +} + +/** + * A login-principal accessor: Apache Shiro `AuthenticationToken.getPrincipal` / + * `getUsername`, Spring Security `Authentication.getName` / `getPrincipal`, or + * `java.security.Principal.getName`. These return the untrusted login identity inside + * an authentication framework. + */ +class AuthPrincipalAccessor extends MethodCall { + AuthPrincipalAccessor() { + exists(Method m | m = this.getMethod() | + m.hasName(["getPrincipal", "getUsername"]) and + m.getDeclaringType() + .getAnAncestor*() + .hasQualifiedName("org.apache.shiro.authc", + ["AuthenticationToken", "UsernamePasswordToken", "HostAuthenticationToken"]) + or + m.hasName(["getName", "getPrincipal"]) and + m.getDeclaringType() + .getAnAncestor*() + .hasQualifiedName("org.springframework.security.core", "Authentication") + or + m.hasName("getName") and + m.getDeclaringType().getAnAncestor*().hasQualifiedName("java.security", "Principal") + ) + } +} + +/** + * A `String` parameter of a DN-builder-shaped method, e.g. `getUserDn`, + * `getUsernameWithSuffix`, `buildDn`, `resolveDn`. An authentication framework + * receives the untrusted principal here and concatenates it into the bind DN. + * + * This source model is name-heuristic: it keys partly off method names. It is a + * deliberate precision/recall trade for the library case, where there is no remote + * flow source to anchor on. A framework that builds the DN in a differently named + * helper is missed; a benign method that matches the name pattern may produce a false + * positive. Triage a result by confirming the value reaches a real bind sink + * unescaped. + */ +class DnBuilderParam extends Parameter { + DnBuilderParam() { + this.getType() instanceof TypeString and + exists(string name | name = this.getCallable().getName().toLowerCase() | + name.matches([ + "get%userdn", "%userdn", "build%dn", "make%dn", "resolve%dn", "create%dn", "to%dn", + "compute%dn" + ]) + or + name.matches("%usernamewithsuffix%") + or + name.matches(["get%principal", "build%principal"]) + ) + } +} + +/** A call to a recognised RFC 2253 DN escaper, e.g. `javax.naming.ldap.Rdn.escapeValue`. */ +class DnEscaperCall extends MethodCall { + DnEscaperCall() { + exists(Method m | m = this.getMethod() | + m.hasName("escapeValue") and + m.getDeclaringType().hasQualifiedName("javax.naming.ldap", "Rdn") + or + m.hasName("nameEncode") and + m.getDeclaringType().hasQualifiedName("org.springframework.ldap.support", "LdapEncoder") + or + m.hasName("encodeForDN") + or + m.getName() + .toLowerCase() + .matches(["%escapedn%", "%escapeldapdn%", "%encodefordn%", "%escapedistinguished%"]) + ) + } +} + +/** + * A taint-tracking configuration for an unescaped login principal flowing into an + * LDAP bind DN inside an authentication framework. + */ +module LdapDnLibraryModeConfig implements DataFlow::ConfigSig { + predicate isSource(DataFlow::Node source) { + source.asExpr() instanceof AuthPrincipalAccessor + or + source.asParameter() instanceof DnBuilderParam + } + + predicate isSink(DataFlow::Node sink) { + exists(MethodCall ma | ma.getMethod() instanceof JndiNameLookupMethod | + sink.asExpr() = ma.getArgument(0) + ) + or + sink.asExpr() = any(SecurityPrincipalPut p).getArgument(1) + or + exists(MethodCall ma | ma.getMethod() instanceof ShiroGetLdapContextMethod | + sink.asExpr() = ma.getArgument(0) + ) + } + + predicate isBarrier(DataFlow::Node node) { node.asExpr() instanceof DnEscaperCall } +} + +module LdapDnLibraryModeFlow = TaintTracking::Global; + +from LdapDnLibraryModeFlow::PathNode source, LdapDnLibraryModeFlow::PathNode sink +where LdapDnLibraryModeFlow::flowPath(source, sink) +select sink.getNode(), source, sink, + "LDAP bind DN depends on a $@ without RFC 2253 distinguished-name escaping.", source.getNode(), + "library-boundary login principal" diff --git a/java/ql/src/experimental/Security/CWE/CWE-090/LdapDnInjectionLibraryModeBad.java b/java/ql/src/experimental/Security/CWE/CWE-090/LdapDnInjectionLibraryModeBad.java new file mode 100644 index 000000000000..e5465cf62dd4 --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-090/LdapDnInjectionLibraryModeBad.java @@ -0,0 +1,21 @@ +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.realm.ldap.LdapContextFactory; + +public class LdapDnInjectionLibraryModeBad { + private final LdapContextFactory ldapContextFactory; + + public LdapDnInjectionLibraryModeBad(LdapContextFactory ldapContextFactory) { + this.ldapContextFactory = ldapContextFactory; + } + + // BAD: the login principal is concatenated into the bind DN with no escaping, so an + // attacker can supply DN metacharacters to manipulate the DN used to authenticate. + protected String getUserDn(String principal) { + return "uid=" + principal + ",ou=people,dc=example,dc=com"; + } + + public Object bind(AuthenticationToken token) throws Exception { + String dn = getUserDn((String) token.getPrincipal()); + return ldapContextFactory.getLdapContext(dn, token.getCredentials()); + } +} diff --git a/java/ql/src/experimental/Security/CWE/CWE-090/LdapDnInjectionLibraryModeGood.java b/java/ql/src/experimental/Security/CWE/CWE-090/LdapDnInjectionLibraryModeGood.java new file mode 100644 index 000000000000..38be555ab17c --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-090/LdapDnInjectionLibraryModeGood.java @@ -0,0 +1,22 @@ +import javax.naming.ldap.Rdn; +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.realm.ldap.LdapContextFactory; + +public class LdapDnInjectionLibraryModeGood { + private final LdapContextFactory ldapContextFactory; + + public LdapDnInjectionLibraryModeGood(LdapContextFactory ldapContextFactory) { + this.ldapContextFactory = ldapContextFactory; + } + + // GOOD: the login principal is escaped for RFC 2253 with Rdn.escapeValue before it + // is placed in the DN, so DN metacharacters are neutralised. + protected String getUserDn(String principal) { + return "uid=" + Rdn.escapeValue(principal) + ",ou=people,dc=example,dc=com"; + } + + public Object bind(AuthenticationToken token) throws Exception { + String dn = getUserDn((String) token.getPrincipal()); + return ldapContextFactory.getLdapContext(dn, token.getCredentials()); + } +} diff --git a/java/ql/test/experimental/query-tests/security/CWE-090/LdapDnInjectionLibraryMode.expected b/java/ql/test/experimental/query-tests/security/CWE-090/LdapDnInjectionLibraryMode.expected new file mode 100644 index 000000000000..8eee85e94281 --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-090/LdapDnInjectionLibraryMode.expected @@ -0,0 +1,75 @@ +#select +| LdapDnInjectionLibraryMode.java:46:41:46:42 | dn | LdapDnInjectionLibraryMode.java:26:30:26:45 | principal : String | LdapDnInjectionLibraryMode.java:46:41:46:42 | dn | LDAP bind DN depends on a $@ without RFC 2253 distinguished-name escaping. | LdapDnInjectionLibraryMode.java:26:30:26:45 | principal | library-boundary login principal | +| LdapDnInjectionLibraryMode.java:46:41:46:42 | dn | LdapDnInjectionLibraryMode.java:44:36:44:55 | getPrincipal(...) : Object | LdapDnInjectionLibraryMode.java:46:41:46:42 | dn | LDAP bind DN depends on a $@ without RFC 2253 distinguished-name escaping. | LdapDnInjectionLibraryMode.java:44:36:44:55 | getPrincipal(...) | library-boundary login principal | +| LdapDnInjectionLibraryMode.java:60:47:60:48 | dn | LdapDnInjectionLibraryMode.java:26:30:26:45 | principal : String | LdapDnInjectionLibraryMode.java:60:47:60:48 | dn | LDAP bind DN depends on a $@ without RFC 2253 distinguished-name escaping. | LdapDnInjectionLibraryMode.java:26:30:26:45 | principal | library-boundary login principal | +| LdapDnInjectionLibraryMode.java:60:47:60:48 | dn | LdapDnInjectionLibraryMode.java:58:27:58:45 | getUsername(...) : String | LdapDnInjectionLibraryMode.java:60:47:60:48 | dn | LDAP bind DN depends on a $@ without RFC 2253 distinguished-name escaping. | LdapDnInjectionLibraryMode.java:58:27:58:45 | getUsername(...) | library-boundary login principal | +| LdapDnInjectionLibraryMode.java:69:35:69:36 | dn | LdapDnInjectionLibraryMode.java:36:42:36:56 | username : String | LdapDnInjectionLibraryMode.java:69:35:69:36 | dn | LDAP bind DN depends on a $@ without RFC 2253 distinguished-name escaping. | LdapDnInjectionLibraryMode.java:36:42:36:56 | username | library-boundary login principal | +| LdapDnInjectionLibraryMode.java:69:35:69:36 | dn | LdapDnInjectionLibraryMode.java:68:48:68:67 | getPrincipal(...) : Object | LdapDnInjectionLibraryMode.java:69:35:69:36 | dn | LDAP bind DN depends on a $@ without RFC 2253 distinguished-name escaping. | LdapDnInjectionLibraryMode.java:68:48:68:67 | getPrincipal(...) | library-boundary login principal | +| LdapDnInjectionLibraryMode.java:84:23:84:24 | dn | LdapDnInjectionLibraryMode.java:26:30:26:45 | principal : String | LdapDnInjectionLibraryMode.java:84:23:84:24 | dn | LDAP bind DN depends on a $@ without RFC 2253 distinguished-name escaping. | LdapDnInjectionLibraryMode.java:26:30:26:45 | principal | library-boundary login principal | +| LdapDnInjectionLibraryMode.java:84:23:84:24 | dn | LdapDnInjectionLibraryMode.java:83:36:83:55 | getPrincipal(...) : Object | LdapDnInjectionLibraryMode.java:84:23:84:24 | dn | LDAP bind DN depends on a $@ without RFC 2253 distinguished-name escaping. | LdapDnInjectionLibraryMode.java:83:36:83:55 | getPrincipal(...) | library-boundary login principal | +edges +| LdapDnInjectionLibraryMode.java:26:30:26:45 | principal : String | LdapDnInjectionLibraryMode.java:27:12:27:50 | ... + ... : String | provenance | | +| LdapDnInjectionLibraryMode.java:26:30:26:45 | principal : String | LdapDnInjectionLibraryMode.java:27:12:27:50 | ... + ... : String | provenance | | +| LdapDnInjectionLibraryMode.java:27:12:27:50 | ... + ... : String | LdapDnInjectionLibraryMode.java:44:17:44:56 | getUserDn(...) : String | provenance | | +| LdapDnInjectionLibraryMode.java:27:12:27:50 | ... + ... : String | LdapDnInjectionLibraryMode.java:58:17:58:46 | getUserDn(...) : String | provenance | | +| LdapDnInjectionLibraryMode.java:27:12:27:50 | ... + ... : String | LdapDnInjectionLibraryMode.java:83:17:83:56 | getUserDn(...) : String | provenance | | +| LdapDnInjectionLibraryMode.java:36:42:36:56 | username : String | LdapDnInjectionLibraryMode.java:37:12:37:36 | ... + ... : String | provenance | | +| LdapDnInjectionLibraryMode.java:36:42:36:56 | username : String | LdapDnInjectionLibraryMode.java:37:12:37:36 | ... + ... : String | provenance | | +| LdapDnInjectionLibraryMode.java:37:12:37:36 | ... + ... : String | LdapDnInjectionLibraryMode.java:68:17:68:68 | getUsernameWithSuffix(...) : String | provenance | | +| LdapDnInjectionLibraryMode.java:44:17:44:56 | getUserDn(...) : String | LdapDnInjectionLibraryMode.java:46:41:46:42 | dn | provenance | | +| LdapDnInjectionLibraryMode.java:44:27:44:55 | (...)... : String | LdapDnInjectionLibraryMode.java:26:30:26:45 | principal : String | provenance | | +| LdapDnInjectionLibraryMode.java:44:27:44:55 | (...)... : String | LdapDnInjectionLibraryMode.java:44:17:44:56 | getUserDn(...) : String | provenance | | +| LdapDnInjectionLibraryMode.java:44:36:44:55 | getPrincipal(...) : Object | LdapDnInjectionLibraryMode.java:44:27:44:55 | (...)... : String | provenance | | +| LdapDnInjectionLibraryMode.java:58:17:58:46 | getUserDn(...) : String | LdapDnInjectionLibraryMode.java:60:47:60:48 | dn | provenance | | +| LdapDnInjectionLibraryMode.java:58:27:58:45 | getUsername(...) : String | LdapDnInjectionLibraryMode.java:26:30:26:45 | principal : String | provenance | | +| LdapDnInjectionLibraryMode.java:58:27:58:45 | getUsername(...) : String | LdapDnInjectionLibraryMode.java:58:17:58:46 | getUserDn(...) : String | provenance | | +| LdapDnInjectionLibraryMode.java:68:17:68:68 | getUsernameWithSuffix(...) : String | LdapDnInjectionLibraryMode.java:69:35:69:36 | dn | provenance | | +| LdapDnInjectionLibraryMode.java:68:39:68:67 | (...)... : String | LdapDnInjectionLibraryMode.java:36:42:36:56 | username : String | provenance | | +| LdapDnInjectionLibraryMode.java:68:39:68:67 | (...)... : String | LdapDnInjectionLibraryMode.java:68:17:68:68 | getUsernameWithSuffix(...) : String | provenance | | +| LdapDnInjectionLibraryMode.java:68:48:68:67 | getPrincipal(...) : Object | LdapDnInjectionLibraryMode.java:68:39:68:67 | (...)... : String | provenance | | +| LdapDnInjectionLibraryMode.java:83:17:83:56 | getUserDn(...) : String | LdapDnInjectionLibraryMode.java:84:23:84:24 | dn | provenance | Sink:MaD:1 | +| LdapDnInjectionLibraryMode.java:83:27:83:55 | (...)... : String | LdapDnInjectionLibraryMode.java:26:30:26:45 | principal : String | provenance | | +| LdapDnInjectionLibraryMode.java:83:27:83:55 | (...)... : String | LdapDnInjectionLibraryMode.java:83:17:83:56 | getUserDn(...) : String | provenance | | +| LdapDnInjectionLibraryMode.java:83:36:83:55 | getPrincipal(...) : Object | LdapDnInjectionLibraryMode.java:83:27:83:55 | (...)... : String | provenance | | +models +| 1 | Sink: javax.naming; Context; true; lookup; ; ; Argument[0]; jndi-injection; manual | +nodes +| LdapDnInjectionLibraryMode.java:26:30:26:45 | principal : String | semmle.label | principal : String | +| LdapDnInjectionLibraryMode.java:26:30:26:45 | principal : String | semmle.label | principal : String | +| LdapDnInjectionLibraryMode.java:27:12:27:50 | ... + ... : String | semmle.label | ... + ... : String | +| LdapDnInjectionLibraryMode.java:27:12:27:50 | ... + ... : String | semmle.label | ... + ... : String | +| LdapDnInjectionLibraryMode.java:36:42:36:56 | username : String | semmle.label | username : String | +| LdapDnInjectionLibraryMode.java:36:42:36:56 | username : String | semmle.label | username : String | +| LdapDnInjectionLibraryMode.java:37:12:37:36 | ... + ... : String | semmle.label | ... + ... : String | +| LdapDnInjectionLibraryMode.java:37:12:37:36 | ... + ... : String | semmle.label | ... + ... : String | +| LdapDnInjectionLibraryMode.java:44:17:44:56 | getUserDn(...) : String | semmle.label | getUserDn(...) : String | +| LdapDnInjectionLibraryMode.java:44:27:44:55 | (...)... : String | semmle.label | (...)... : String | +| LdapDnInjectionLibraryMode.java:44:36:44:55 | getPrincipal(...) : Object | semmle.label | getPrincipal(...) : Object | +| LdapDnInjectionLibraryMode.java:46:41:46:42 | dn | semmle.label | dn | +| LdapDnInjectionLibraryMode.java:58:17:58:46 | getUserDn(...) : String | semmle.label | getUserDn(...) : String | +| LdapDnInjectionLibraryMode.java:58:27:58:45 | getUsername(...) : String | semmle.label | getUsername(...) : String | +| LdapDnInjectionLibraryMode.java:60:47:60:48 | dn | semmle.label | dn | +| LdapDnInjectionLibraryMode.java:68:17:68:68 | getUsernameWithSuffix(...) : String | semmle.label | getUsernameWithSuffix(...) : String | +| LdapDnInjectionLibraryMode.java:68:39:68:67 | (...)... : String | semmle.label | (...)... : String | +| LdapDnInjectionLibraryMode.java:68:48:68:67 | getPrincipal(...) : Object | semmle.label | getPrincipal(...) : Object | +| LdapDnInjectionLibraryMode.java:69:35:69:36 | dn | semmle.label | dn | +| LdapDnInjectionLibraryMode.java:83:17:83:56 | getUserDn(...) : String | semmle.label | getUserDn(...) : String | +| LdapDnInjectionLibraryMode.java:83:27:83:55 | (...)... : String | semmle.label | (...)... : String | +| LdapDnInjectionLibraryMode.java:83:36:83:55 | getPrincipal(...) : Object | semmle.label | getPrincipal(...) : Object | +| LdapDnInjectionLibraryMode.java:84:23:84:24 | dn | semmle.label | dn | +subpaths +| LdapDnInjectionLibraryMode.java:44:27:44:55 | (...)... : String | LdapDnInjectionLibraryMode.java:26:30:26:45 | principal : String | LdapDnInjectionLibraryMode.java:27:12:27:50 | ... + ... : String | LdapDnInjectionLibraryMode.java:44:17:44:56 | getUserDn(...) : String | +| LdapDnInjectionLibraryMode.java:58:27:58:45 | getUsername(...) : String | LdapDnInjectionLibraryMode.java:26:30:26:45 | principal : String | LdapDnInjectionLibraryMode.java:27:12:27:50 | ... + ... : String | LdapDnInjectionLibraryMode.java:58:17:58:46 | getUserDn(...) : String | +| LdapDnInjectionLibraryMode.java:68:39:68:67 | (...)... : String | LdapDnInjectionLibraryMode.java:36:42:36:56 | username : String | LdapDnInjectionLibraryMode.java:37:12:37:36 | ... + ... : String | LdapDnInjectionLibraryMode.java:68:17:68:68 | getUsernameWithSuffix(...) : String | +| LdapDnInjectionLibraryMode.java:83:27:83:55 | (...)... : String | LdapDnInjectionLibraryMode.java:26:30:26:45 | principal : String | LdapDnInjectionLibraryMode.java:27:12:27:50 | ... + ... : String | LdapDnInjectionLibraryMode.java:83:17:83:56 | getUserDn(...) : String | +testFailures +| LdapDnInjectionLibraryMode.java:26:30:26:45 | principal : String | Unexpected result: Source | +| LdapDnInjectionLibraryMode.java:36:42:36:56 | username : String | Unexpected result: Source | +| LdapDnInjectionLibraryMode.java:43:52:43:62 | // $ Source | Missing result: Source | +| LdapDnInjectionLibraryMode.java:44:36:44:55 | getPrincipal(...) : Object | Unexpected result: Source | +| LdapDnInjectionLibraryMode.java:57:64:57:74 | // $ Source | Missing result: Source | +| LdapDnInjectionLibraryMode.java:58:27:58:45 | getUsername(...) : String | Unexpected result: Source | +| LdapDnInjectionLibraryMode.java:66:86:66:96 | // $ Source | Missing result: Source | +| LdapDnInjectionLibraryMode.java:68:48:68:67 | getPrincipal(...) : Object | Unexpected result: Source | +| LdapDnInjectionLibraryMode.java:82:92:82:102 | // $ Source | Missing result: Source | +| LdapDnInjectionLibraryMode.java:83:36:83:55 | getPrincipal(...) : Object | Unexpected result: Source | diff --git a/java/ql/test/experimental/query-tests/security/CWE-090/LdapDnInjectionLibraryMode.java b/java/ql/test/experimental/query-tests/security/CWE-090/LdapDnInjectionLibraryMode.java new file mode 100644 index 000000000000..e4ba4915057f --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-090/LdapDnInjectionLibraryMode.java @@ -0,0 +1,92 @@ +// Fixture for java/ldap-dn-injection-library-mode. The login principal enters as a +// method parameter inside an authentication framework (no remote flow source), so the +// supported java/ldap-injection query has no source here. The library-mode query +// models the DN-builder parameter and the auth-token principal accessor as sources and +// the bind-DN positions as sinks. The shape mirrors Apache Shiro CVE-2026-49268. + +import java.util.Hashtable; + +import javax.naming.Context; +import javax.naming.NamingException; +import javax.naming.ldap.LdapContext; +import javax.naming.ldap.Rdn; + +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.authc.UsernamePasswordToken; +import org.apache.shiro.realm.ldap.LdapContextFactory; + +public class LdapDnInjectionLibraryMode { + + private String userDnPrefix = "uid="; + private String userDnSuffix = ",ou=people,dc=example,dc=com"; + + // ---- DN builders: the principal arrives as a String parameter (library-mode source) ---- + + // BAD: principal concatenated straight into the bind DN, no escaping. + protected String getUserDn(String principal) { + return userDnPrefix + principal + userDnSuffix; + } + + // GOOD: principal wrapped in Rdn.escapeValue (the canonical 2.2.1 fix). + protected String getUserDnSafe(String principal) { + return userDnPrefix + Rdn.escapeValue(principal) + userDnSuffix; + } + + // BAD: ActiveDirectory-style "username with suffix" -- verbatim concatenation. + protected String getUsernameWithSuffix(String username) { + return username + "@example.com"; + } + + // ---- Sink: Context.SECURITY_PRINCIPAL environment value ---- + + // BAD: tainted DN flows into the SECURITY_PRINCIPAL env value. + public void bindBad(AuthenticationToken token) { // $ Source + String dn = getUserDn((String) token.getPrincipal()); + Hashtable env = new Hashtable(); + env.put(Context.SECURITY_PRINCIPAL, dn); // $ Alert + } + + // GOOD: same path but through the escaping builder. + public void bindGood(AuthenticationToken token) { + String dn = getUserDnSafe((String) token.getPrincipal()); + Hashtable env = new Hashtable(); + env.put(Context.SECURITY_PRINCIPAL, dn); // safe + } + + // BAD: same sink using the literal property key and a UsernamePasswordToken. + public void bindBadLiteralKey(UsernamePasswordToken token) { // $ Source + String dn = getUserDn(token.getUsername()); + Hashtable env = new Hashtable(); + env.put("java.naming.security.principal", dn); // $ Alert + } + + // ---- Sink: Shiro LdapContextFactory.getLdapContext(principal, credentials) ---- + + // BAD: the exact CVE-2026-49268 sink -- tainted DN as the bind principal. + public LdapContext queryBad(LdapContextFactory factory, AuthenticationToken token) // $ Source + throws NamingException { + String dn = getUsernameWithSuffix((String) token.getPrincipal()); + return factory.getLdapContext(dn, token.getCredentials()); // $ Alert + } + + // GOOD: principal escaped before the bind. + public LdapContext queryGood(LdapContextFactory factory, AuthenticationToken token) + throws NamingException { + String dn = userDnPrefix + Rdn.escapeValue(token.getPrincipal()) + userDnSuffix; + return factory.getLdapContext(dn, token.getCredentials()); // safe + } + + // ---- Sink: Context lookup with a String name ---- + + // BAD: tainted DN used as a String name for a context lookup. + public Object lookupBad(Context ctx, AuthenticationToken token) throws NamingException { // $ Source + String dn = getUserDn((String) token.getPrincipal()); + return ctx.lookup(dn); // $ Alert + } + + // GOOD: escaped before lookup. + public Object lookupGood(Context ctx, AuthenticationToken token) throws NamingException { + String dn = getUserDnSafe((String) token.getPrincipal()); + return ctx.lookup(dn); // safe + } +} diff --git a/java/ql/test/experimental/query-tests/security/CWE-090/LdapDnInjectionLibraryMode.qlref b/java/ql/test/experimental/query-tests/security/CWE-090/LdapDnInjectionLibraryMode.qlref new file mode 100644 index 000000000000..f3083d6fca06 --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-090/LdapDnInjectionLibraryMode.qlref @@ -0,0 +1,4 @@ +query: experimental/Security/CWE/CWE-090/LdapDnInjectionLibraryMode.ql +postprocess: + - utils/test/PrettyPrintModels.ql + - utils/test/InlineExpectationsTestQuery.ql diff --git a/java/ql/test/experimental/query-tests/security/CWE-090/options b/java/ql/test/experimental/query-tests/security/CWE-090/options new file mode 100644 index 000000000000..3c81954969ca --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-090/options @@ -0,0 +1 @@ +//semmle-extractor-options: --javac-args -cp ${testdir}/../../../../experimental/stubs/org-apache-shiro-authc-2.0.1:${testdir}/../../../../experimental/stubs/org-apache-shiro-realm-ldap-2.0.1 diff --git a/java/ql/test/experimental/stubs/org-apache-shiro-authc-2.0.1/org/apache/shiro/authc/UsernamePasswordToken.java b/java/ql/test/experimental/stubs/org-apache-shiro-authc-2.0.1/org/apache/shiro/authc/UsernamePasswordToken.java new file mode 100644 index 000000000000..037220faf567 --- /dev/null +++ b/java/ql/test/experimental/stubs/org-apache-shiro-authc-2.0.1/org/apache/shiro/authc/UsernamePasswordToken.java @@ -0,0 +1,15 @@ +// Generated automatically from org.apache.shiro.authc.UsernamePasswordToken for testing purposes + +package org.apache.shiro.authc; + +import org.apache.shiro.authc.HostAuthenticationToken; + +public class UsernamePasswordToken implements HostAuthenticationToken +{ + public UsernamePasswordToken() {} + public Object getPrincipal() { return null; } + public Object getCredentials() { return null; } + public String getHost() { return null; } + public String getUsername() { return null; } + public char[] getPassword() { return null; } +} diff --git a/java/ql/test/experimental/stubs/org-apache-shiro-realm-ldap-2.0.1/org/apache/shiro/realm/ldap/LdapContextFactory.java b/java/ql/test/experimental/stubs/org-apache-shiro-realm-ldap-2.0.1/org/apache/shiro/realm/ldap/LdapContextFactory.java new file mode 100644 index 000000000000..b186e56562c7 --- /dev/null +++ b/java/ql/test/experimental/stubs/org-apache-shiro-realm-ldap-2.0.1/org/apache/shiro/realm/ldap/LdapContextFactory.java @@ -0,0 +1,13 @@ +// Generated automatically from org.apache.shiro.realm.ldap.LdapContextFactory for testing purposes + +package org.apache.shiro.realm.ldap; + +import javax.naming.NamingException; +import javax.naming.ldap.LdapContext; + +public interface LdapContextFactory +{ + LdapContext getSystemLdapContext() throws NamingException; + + LdapContext getLdapContext(Object principal, Object credentials) throws NamingException; +}