From e53e018771bf9e30558e21950a8cbcd4c567e103 Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Wed, 08 Jul 2026 16:08:15 +0000
Subject: [PATCH] [#673] Fix ArrayIndexOutOfBoundsException on truncated percent-encoding in LDAP URLs (#676)

---
 opendj-server-legacy/src/test/java/org/opends/server/authorization/dseecompat/UserDNTestCase.java |   56 ++++++++++++++++++
 opendj-core/src/test/java/org/forgerock/opendj/ldap/LDAPUrlTestCase.java                          |   30 ++++++++++
 opendj-server-legacy/src/main/java/org/opends/server/types/LDAPURL.java                           |    3 
 opendj-server-legacy/src/test/java/org/opends/server/types/LDAPURLTestCase.java                   |   35 +++++++++++
 opendj-core/src/main/java/org/forgerock/opendj/ldap/LDAPUrl.java                                  |    7 ++
 5 files changed, 130 insertions(+), 1 deletions(-)

diff --git a/opendj-core/src/main/java/org/forgerock/opendj/ldap/LDAPUrl.java b/opendj-core/src/main/java/org/forgerock/opendj/ldap/LDAPUrl.java
index f30d9a0..9bc4947 100644
--- a/opendj-core/src/main/java/org/forgerock/opendj/ldap/LDAPUrl.java
+++ b/opendj-core/src/main/java/org/forgerock/opendj/ldap/LDAPUrl.java
@@ -13,6 +13,7 @@
  *
  * Copyright 2010 Sun Microsystems, Inc.
  * Portions copyright 2012-2016 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC
  */
 package org.forgerock.opendj.ldap;
 
@@ -246,6 +247,12 @@
                 dstPos++;
                 continue;
             }
+            if (srcPos + 2 >= decoded.length()) {
+                // A percent sign must be followed by two hexadecimal digits.
+                final LocalizableMessage msg =
+                        ERR_LDAPURL_INVALID_HEX_BYTE.get(urlString, index + srcPos + 1);
+                throw new LocalizedIllegalArgumentException(msg);
+            }
             int i = decodeHex(urlString, index + srcPos + 1, decoded.charAt(srcPos + 1)) << 4;
             int j = decodeHex(urlString, index + srcPos + 2, decoded.charAt(srcPos + 2));
             decoded.setCharAt(dstPos, (char) (i | j));
diff --git a/opendj-core/src/test/java/org/forgerock/opendj/ldap/LDAPUrlTestCase.java b/opendj-core/src/test/java/org/forgerock/opendj/ldap/LDAPUrlTestCase.java
index a316d9b..1c3020e 100644
--- a/opendj-core/src/test/java/org/forgerock/opendj/ldap/LDAPUrlTestCase.java
+++ b/opendj-core/src/test/java/org/forgerock/opendj/ldap/LDAPUrlTestCase.java
@@ -12,6 +12,7 @@
  * information: "Portions Copyright [year] [name of copyright owner]".
  *
  * Copyright 2010 Sun Microsystems, Inc.
+ * Portions Copyright 2026 3A Systems, LLC
  */
 
 package org.forgerock.opendj.ldap;
@@ -19,6 +20,7 @@
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertTrue;
 
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
 import org.testng.annotations.DataProvider;
 import org.testng.annotations.Test;
 
@@ -181,4 +183,32 @@
             assertTrue(url1.equals(url2));
         }
     }
+
+    /**
+     * URLs with a percent sign followed by fewer than two hexadecimal digits.
+     *
+     * @return The malformed URLs.
+     */
+    @DataProvider
+    public Object[][] truncatedPercentUrls() {
+        return new Object[][] {
+            { "ldap:///cn=name%B" },
+            { "ldap:///cn=name%" },
+        };
+    }
+
+    /**
+     * A truncated percent-encoded sequence must be rejected with a clean decode error
+     * instead of a StringIndexOutOfBoundsException - see issue #673.
+     *
+     * @param url
+     *            The URL to decode.
+     * @throws Exception
+     *             If the test failed unexpectedly.
+     */
+    @Test(dataProvider = "truncatedPercentUrls",
+            expectedExceptions = LocalizedIllegalArgumentException.class)
+    public void testTruncatedPercentEncoding(final String url) throws Exception {
+        LDAPUrl.valueOf(url);
+    }
 }
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/types/LDAPURL.java b/opendj-server-legacy/src/main/java/org/opends/server/types/LDAPURL.java
index 8b4c50a..c16e437 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/types/LDAPURL.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/types/LDAPURL.java
@@ -13,6 +13,7 @@
  *
  * Copyright 2006-2008 Sun Microsystems, Inc.
  * Portions Copyright 2012-2016 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC
  */
 package org.opends.server.types;
 
@@ -621,7 +622,7 @@
       {
         // There must be at least two bytes left.  If not, then that's
         // a problem.
-        if (i+2 > length)
+        if (i+2 >= length)
         {
           LocalizableMessage message = ERR_LDAPURL_PERCENT_TOO_CLOSE_TO_END.get(s, i);
           throw new DirectoryException(
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/authorization/dseecompat/UserDNTestCase.java b/opendj-server-legacy/src/test/java/org/opends/server/authorization/dseecompat/UserDNTestCase.java
new file mode 100644
index 0000000..fac597f
--- /dev/null
+++ b/opendj-server-legacy/src/test/java/org/opends/server/authorization/dseecompat/UserDNTestCase.java
@@ -0,0 +1,56 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2026 3A Systems, LLC
+ */
+package org.opends.server.authorization.dseecompat;
+
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DN;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/** Tests for decoding of the userdn bind rule. */
+public class UserDNTestCase extends AciTestCase
+{
+  /**
+   * ACIs whose userdn LDAP URL contains a percent sign followed by fewer than
+   * two hexadecimal digits - see issue #673.
+   *
+   * @return The malformed ACIs.
+   */
+  @DataProvider
+  public Object[][] truncatedPercentAcis()
+  {
+    return new Object[][] {
+      { "(version 3.0; acl \":\"; allow (search) userdn=\"ldap://cn=name%B\"; )" },
+      { "(version 3.0; acl \":\"; allow (search) userdn=\"ldap://cn=name%\"; )" },
+    };
+  }
+
+  /**
+   * A truncated percent-encoded sequence in the userdn URL must be rejected
+   * with a clean AciException instead of an ArrayIndexOutOfBoundsException -
+   * see issue #673.
+   *
+   * @param aciString
+   *          The ACI to decode.
+   * @throws Exception
+   *           If an unexpected exception occurred.
+   */
+  @Test(dataProvider = "truncatedPercentAcis", expectedExceptions = AciException.class)
+  public void testTruncatedPercentInUserDN(String aciString) throws Exception
+  {
+    Aci.decode(ByteString.valueOfUtf8(aciString), DN.rootDN());
+  }
+}
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/types/LDAPURLTestCase.java b/opendj-server-legacy/src/test/java/org/opends/server/types/LDAPURLTestCase.java
index 7e82bbb..223ca31 100644
--- a/opendj-server-legacy/src/test/java/org/opends/server/types/LDAPURLTestCase.java
+++ b/opendj-server-legacy/src/test/java/org/opends/server/types/LDAPURLTestCase.java
@@ -12,6 +12,7 @@
  * information: "Portions Copyright [year] [name of copyright owner]".
  *
  * Portions Copyright 2012-2016 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC
  */
 package org.opends.server.types;
 
@@ -98,4 +99,38 @@
     assertEquals(url.getRawFilter(), filterString);
   }
 
+
+
+  /**
+   * Test data for testTruncatedPercentEncoding.
+   *
+   * @return URLs with a percent sign followed by fewer than two hexadecimal digits.
+   */
+  @DataProvider
+  public Object[][] truncatedPercentData()
+  {
+    return new Object[][] {
+        { "ldap:///cn=name%B" },
+        { "ldap:///cn=name%" },
+    };
+  }
+
+
+
+  /**
+   * A percent sign followed by fewer than two hexadecimal digits must be rejected with a
+   * clean decode error instead of an ArrayIndexOutOfBoundsException - see issue #673.
+   *
+   * @param urlString
+   *          The URL to decode.
+   * @throws Exception
+   *           If an unexpected exception occurred.
+   */
+  @Test(dataProvider = "truncatedPercentData",
+        expectedExceptions = DirectoryException.class)
+  public void testTruncatedPercentEncoding(String urlString) throws Exception
+  {
+    LDAPURL.decode(urlString, true);
+  }
+
 }

--
Gitblit v1.10.0