From 08aee4724608e4a32baa3c7d7499ec913a275aaf Mon Sep 17 00:00:00 2001 From: Maxim Thomas Date: Mon, 3 Mar 2025 20:17:50 +0300 Subject: [PATCH] CVE-2025-27497 Fix Denial of Service (Dos) using alias loop --- .../LocalBackendSearchOperation.java | 22 +++- .../opendj/AliasTestCase.java | 101 +++++++++++++++++- 2 files changed, 116 insertions(+), 7 deletions(-) diff --git a/opendj-server-legacy/src/main/java/org/opends/server/workflowelement/localbackend/LocalBackendSearchOperation.java b/opendj-server-legacy/src/main/java/org/opends/server/workflowelement/localbackend/LocalBackendSearchOperation.java index 408e7e1426..cec674da97 100644 --- a/opendj-server-legacy/src/main/java/org/opends/server/workflowelement/localbackend/LocalBackendSearchOperation.java +++ b/opendj-server-legacy/src/main/java/org/opends/server/workflowelement/localbackend/LocalBackendSearchOperation.java @@ -13,10 +13,12 @@ * * Copyright 2008-2010 Sun Microsystems, Inc. * Portions Copyright 2011-2016 ForgeRock AS. - * Portions Copyright 2024 3A Systems, LLC. + * Portions Copyright 2024-2025 3A Systems, LLC. */ package org.opends.server.workflowelement.localbackend; +import java.util.HashSet; +import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import org.forgerock.i18n.slf4j.LocalizedLogger; @@ -67,6 +69,9 @@ public class LocalBackendSearchOperation /** The filter for the search. */ private SearchFilter filter; + /** Service object to detect dereferencing recursion */ + private final Set dereferencingDNs = new HashSet<>(); + /** * Creates a new operation that may be used to search for entries in a local * backend of the Directory Server. @@ -207,9 +212,18 @@ private void processSearch(AtomicBoolean executePostOpPlugins) throws CanceledOp ) { final Entry baseEntry=DirectoryServer.getEntry(baseDN); if (baseEntry!=null && baseEntry.isAlias()) { - setBaseDN(baseEntry.getAliasedDN()); - processSearch(executePostOpPlugins); - return; + final DN aliasedDn = baseEntry.getAliasedDN(); + if(!dereferencingDNs.contains(aliasedDn)) { //detect recursive search + dereferencingDNs.add(aliasedDn); + setBaseDN(aliasedDn); + try { + processSearch(executePostOpPlugins); + } catch (StackOverflowError error) { + throw new Exception(error); + } + dereferencingDNs.remove(aliasedDn); + return; + } } } diff --git a/opendj-server-legacy/src/test/java/org/openidentityplatform/opendj/AliasTestCase.java b/opendj-server-legacy/src/test/java/org/openidentityplatform/opendj/AliasTestCase.java index ee248b1f53..99376f8dc6 100644 --- a/opendj-server-legacy/src/test/java/org/openidentityplatform/opendj/AliasTestCase.java +++ b/opendj-server-legacy/src/test/java/org/openidentityplatform/opendj/AliasTestCase.java @@ -11,7 +11,7 @@ * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions Copyright [year] [name of copyright owner]". * - * Copyright 2024 3A Systems, LLC. + * Copyright 2024-2025 3A Systems, LLC. */ package org.openidentityplatform.opendj; @@ -23,6 +23,7 @@ import org.opends.server.DirectoryServerTestCase; import org.opends.server.TestCaseUtils; +import org.opends.server.types.Entry; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; @@ -60,6 +61,61 @@ public void startServer() throws Exception { "objectclass: extensibleobject", "cn: President", "aliasedobjectname: cn=John Doe, o=MyCompany, o=test", + "", + + "dn: ou=employees,o=test", + "objectClass: top", + "objectClass: organizationalUnit", + "ou: employees", + "description: All employees", + "", + "dn: uid=jdoe,ou=employees,o=test", + "objectClass: alias", + "objectClass: top", + "objectClass: extensibleObject", + "aliasedObjectName: uid=jdoe,ou=researchers,o=test", + "uid: jdoe", + "", + "dn: ou=researchers,o=test", + "objectClass: top", + "objectClass: organizationalUnit", + "ou: researchers", + "description: All reasearchers", + "", + "dn: uid=jdoe,ou=researchers,o=test", + "objectClass: alias", + "objectClass: top", + "objectClass: extensibleObject", + "aliasedObjectName: uid=jdoe,ou=employees,o=test", + "uid: jdoe", + + "", + "dn: ou=students,o=test", + "objectClass: top", + "objectClass: organizationalUnit", + "ou: students", + "description: All students", + "", + "dn: uid=janedoe,ou=students,o=test", + "objectClass: alias", + "objectClass: top", + "objectClass: extensibleObject", + "aliasedObjectName: uid=janedoe,ou=researchers,o=test", + "uid: janedoe", + "", + "dn: uid=janedoe,ou=researchers,o=test", + "objectClass: alias", + "objectClass: top", + "objectClass: extensibleObject", + "aliasedObjectName: uid=janedoe,ou=employees,o=test", + "uid: janedoe", + "", + "dn: uid=janedoe,ou=employees,o=test", + "objectClass: alias", + "objectClass: top", + "objectClass: extensibleObject", + "aliasedObjectName: uid=janedoe,ou=students,o=test", + "uid: janedoe", "" ); @@ -70,7 +126,11 @@ public void startServer() throws Exception { } public HashMap search(SearchScope scope,DereferenceAliasesPolicy policy) throws SearchResultReferenceIOException, LdapException { - final SearchRequest request =Requests.newSearchRequest("ou=Area1,o=test", scope,"(objectclass=*)") + return search("ou=Area1,o=test", scope, policy); + } + + public HashMap search(String dn, SearchScope scope,DereferenceAliasesPolicy policy) throws SearchResultReferenceIOException, LdapException { + final SearchRequest request =Requests.newSearchRequest(dn, scope,"(objectclass=*)") .setDereferenceAliasesPolicy(policy); System.out.println("---------------------------------------------------------------------------------------"); System.out.println(request); @@ -125,7 +185,7 @@ public void test_base_find() throws SearchResultReferenceIOException, LdapExcept // It returns ou=Area1,o=test. @Test public void test_base_search() throws SearchResultReferenceIOException, LdapException { - HashMap res=search(SearchScope.BASE_OBJECT,DereferenceAliasesPolicy.IN_SEARCHING); + HashMap res=search(SearchScope.BASE_OBJECT, DereferenceAliasesPolicy.IN_SEARCHING); assertThat(res.containsKey("ou=Area1,o=test")).isTrue(); assertThat(res.containsKey("o=MyCompany,o=test")).isFalse(); @@ -308,4 +368,39 @@ public void test_sub_always() throws SearchResultReferenceIOException, LdapExcep assertThat(res.containsKey("cn=John Doe,o=MyCompany,o=test")).isTrue(); } + // Dereferencing recursion avoidance test. + @Test + public void test_alias_recursive() throws LdapException, SearchResultReferenceIOException { + HashMap res = search("uid=jdoe,ou=employees,o=test", SearchScope.WHOLE_SUBTREE, DereferenceAliasesPolicy.ALWAYS); + + assertThat(res.containsKey("uid=jdoe,ou=employees,o=test")).isTrue(); + assertThat(res.containsKey("uid=jdoe,ou=researchers,o=test")).isFalse(); + } + + @Test + public void test_alias_recursive_loop() throws LdapException, SearchResultReferenceIOException { + HashMap res = search("uid=janedoe,ou=students,o=test", SearchScope.WHOLE_SUBTREE, DereferenceAliasesPolicy.ALWAYS); + + assertThat(res.containsKey("uid=janedoe,ou=students,o=test")).isTrue(); + assertThat(res.containsKey("uid=janedoe,ou=researches,o=test")).isFalse(); + assertThat(res.containsKey("uid=janedoe,ou=employees,o=test")).isFalse(); + } + + @Test(expectedExceptions = LdapException.class) + public void test_stackoverflow() throws Exception { + + String entryTemplate = "dn: uid={uid},ou=employees,o=test\n" + + "objectClass: alias\n" + + "objectClass: top\n" + + "objectClass: extensibleObject\n" + + "aliasedObjectName: uid={alias},ou=employees,o=test \n" + + "uid: {uid}\n"; + final String firstDn = "uid=jdoe0,ou=employees,o=test"; + for(int i = 0; i < 10000; i++) { + String entryStr = entryTemplate.replace("{uid}", "jdoe" + i).replace("{alias}", "jdoe" + (i + 1)); + Entry entry = TestCaseUtils.makeEntry(entryStr); + TestCaseUtils.addEntry(entry); + } + search(firstDn, SearchScope.WHOLE_SUBTREE, DereferenceAliasesPolicy.ALWAYS); + } }