diff --git a/core/src/main/java/org/apache/shiro/realm/activedirectory/ActiveDirectoryRealm.java b/core/src/main/java/org/apache/shiro/realm/activedirectory/ActiveDirectoryRealm.java index 3dc459cb33..1a58c2941a 100644 --- a/core/src/main/java/org/apache/shiro/realm/activedirectory/ActiveDirectoryRealm.java +++ b/core/src/main/java/org/apache/shiro/realm/activedirectory/ActiveDirectoryRealm.java @@ -39,6 +39,7 @@ import javax.naming.directory.SearchControls; import javax.naming.directory.SearchResult; import javax.naming.ldap.LdapContext; +import javax.naming.ldap.Rdn; import java.util.*; @@ -162,7 +163,7 @@ protected Set getRoleNamesForUser(String username, LdapContext ldapConte SearchControls searchCtls = new SearchControls(); searchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE); - String userPrincipalName = username; + String userPrincipalName = Rdn.escapeValue(username); if (principalSuffix != null && !userPrincipalName.toLowerCase(Locale.ROOT).endsWith(principalSuffix.toLowerCase(Locale.ROOT))) { userPrincipalName += principalSuffix; } diff --git a/core/src/main/java/org/apache/shiro/realm/ldap/DefaultLdapRealm.java b/core/src/main/java/org/apache/shiro/realm/ldap/DefaultLdapRealm.java index dec43a94d2..62ae56f106 100644 --- a/core/src/main/java/org/apache/shiro/realm/ldap/DefaultLdapRealm.java +++ b/core/src/main/java/org/apache/shiro/realm/ldap/DefaultLdapRealm.java @@ -35,6 +35,7 @@ import javax.naming.AuthenticationNotSupportedException; import javax.naming.NamingException; import javax.naming.ldap.LdapContext; +import javax.naming.ldap.Rdn; /** * An LDAP {@link org.apache.shiro.realm.Realm Realm} implementation utilizing Sun's/Oracle's @@ -239,11 +240,12 @@ protected String getUserDn(String principal) throws IllegalArgumentException, Il int prefixLength = prefix != null ? prefix.length() : 0; int suffixLength = suffix != null ? suffix.length() : 0; - StringBuilder sb = new StringBuilder(prefixLength + principal.length() + suffixLength); + String sanitizedPrincipal = Rdn.escapeValue(principal); + StringBuilder sb = new StringBuilder(prefixLength + sanitizedPrincipal.length() + suffixLength); if (prefixLength > 0) { sb.append(prefix); } - sb.append(principal); + sb.append(sanitizedPrincipal); if (suffixLength > 0) { sb.append(suffix); } diff --git a/core/src/test/java/org/apache/shiro/realm/activedirectory/ActiveDirectoryRealmTest.java b/core/src/test/java/org/apache/shiro/realm/activedirectory/ActiveDirectoryRealmTest.java index 727c541915..ce08f1e3af 100644 --- a/core/src/test/java/org/apache/shiro/realm/activedirectory/ActiveDirectoryRealmTest.java +++ b/core/src/test/java/org/apache/shiro/realm/activedirectory/ActiveDirectoryRealmTest.java @@ -130,6 +130,42 @@ public void testExistingUserSuffix() throws Exception { assertExistingUserSuffix(USERNAME + "@EXAMPLE.com", "testuser@EXAMPLE.com"); } + /** + * Regression test for CVE-2026-49268 (LDAP injection): {@link ActiveDirectoryRealm} must escape + * user-supplied input per RFC 2253 before using it to build the LDAP query argument, so an + * attacker cannot inject extra RDN components / alter the query structure. + *

+ * Without the {@code Rdn.escapeValue(username)} fix the captured argument is the raw + * {@code testuser,ou=admins} (unescaped ',' and '='). + */ + @Test + public void testGetRoleNamesForUserEscapesInjectionCharacters() throws Exception { + + LdapContext ldapContext = createMock(LdapContext.class); + NamingEnumeration results = createMock(NamingEnumeration.class); + Capture captureArgs = Capture.newInstance(CaptureType.ALL); + expect(ldapContext.search(anyString(), anyString(), capture(captureArgs), anyObject(SearchControls.class))).andReturn(results); + replay(ldapContext); + + ActiveDirectoryRealm activeDirectoryRealm = new ActiveDirectoryRealm(); + + SecurityManager securityManager = new DefaultSecurityManager(activeDirectoryRealm); + Subject subject = new Subject.Builder(securityManager).buildSubject(); + subject.execute(() -> { + + try { + activeDirectoryRealm.getRoleNamesForUser("testuser,ou=admins", ldapContext); + } catch (NamingException e) { + Assert.fail("Unexpected NamingException thrown during test"); + } + }); + + Object[] args = captureArgs.getValue(); + assertThat(args, arrayWithSize(1)); + // injected ',' and '=' are backslash-escaped, so they cannot break out of the value + assertThat(args[0], is("testuser\\,ou\\=admins")); + } + public void assertExistingUserSuffix(String username, String expectedPrincipalName) throws Exception { LdapContext ldapContext = createMock(LdapContext.class); diff --git a/core/src/test/java/org/apache/shiro/realm/ldap/DefaultLdapRealmTest.java b/core/src/test/java/org/apache/shiro/realm/ldap/DefaultLdapRealmTest.java index a94927d798..cb4a448745 100644 --- a/core/src/test/java/org/apache/shiro/realm/ldap/DefaultLdapRealmTest.java +++ b/core/src/test/java/org/apache/shiro/realm/ldap/DefaultLdapRealmTest.java @@ -176,4 +176,33 @@ protected String getUserDnSuffix() { String userDn = realm.getUserDn(principal); assertEquals(principal, userDn); } + + /** + * A normal username must be substituted into the template unchanged so that the fix for + * CVE-2026-49268 does not alter legitimate authentication behavior. + */ + @Test + public void testGetUserDnLeavesOrdinaryUsernameUnchanged() { + realm.setUserDnTemplate("uid={0},ou=users,dc=mycompany,dc=com"); + assertEquals("uid=jsmith,ou=users,dc=mycompany,dc=com", realm.getUserDn("jsmith")); + } + + /** + * Regression test for CVE-2026-49268 (LDAP DN injection): user-supplied input must be escaped + * per RFC 2253 before being concatenated into the user DN, so an attacker cannot inject extra + * RDN components and alter the DN structure used for the bind. + *

+ * Without the {@code Rdn.escapeValue(principal)} fix this returns + * {@code uid=jsmith,ou=admins,ou=users,dc=mycompany,dc=com} (two uid/ou breakouts). + */ + @Test + public void testGetUserDnEscapesInjectionCharacters() { + realm.setUserDnTemplate("uid={0},ou=users,dc=mycompany,dc=com"); + + // Attacker attempts to break out of the uid RDN and graft on a privileged subtree. + String userDn = realm.getUserDn("jsmith,ou=admins"); + + // The injected ',' and '=' are backslash-escaped, leaving exactly one uid RDN intact. + assertEquals("uid=jsmith\\,ou\\=admins,ou=users,dc=mycompany,dc=com", userDn); + } }