Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -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.*;


Expand Down Expand Up @@ -162,7 +163,7 @@ protected Set<String> 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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* <p/>
* 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<SearchResult> results = createMock(NamingEnumeration.class);
Capture<Object[]> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* <p>
* 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);
}
}
Loading