From 07bad95016f2cfafaf217114f9b0b50429af7d2f Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Sun, 12 Jul 2026 12:06:03 +0000
Subject: [PATCH] [#726] Reject malformed bracketed IPv6 hosts in HostPort (#732)

---
 opendj-server-legacy/src/test/java/org/opends/server/types/HostPortTest.java                      |   66 ++++++++++++++++++++++
 opendj-server-legacy/src/test/java/org/opends/server/authorization/dseecompat/UserDNTestCase.java |   45 +++++++++++++++
 opendj-server-legacy/src/main/java/org/opends/server/types/HostPort.java                          |   37 +++++++++---
 3 files changed, 138 insertions(+), 10 deletions(-)

diff --git a/opendj-server-legacy/src/main/java/org/opends/server/types/HostPort.java b/opendj-server-legacy/src/main/java/org/opends/server/types/HostPort.java
index fabb3df..bff0143 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/types/HostPort.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/types/HostPort.java
@@ -13,7 +13,7 @@
  *
  * Copyright 2006-2008 Sun Microsystems, Inc.
  * Portions Copyright 2013-2016 ForgeRock AS.
- * Portions Copyright 2025 3A Systems LLC.
+ * Portions Copyright 2025-2026 3A Systems LLC.
  */
 package org.opends.server.types;
 
@@ -229,8 +229,9 @@
    * @throws NumberFormatException
    *           If the "port" in the supplied string cannot be converted to an int
    * @throws IllegalArgumentException
-   *           if no port could be found in the supplied string, or if the port
-   *           is not a valid port number
+   *           if no port could be found in the supplied string, if the port
+   *           is not a valid port number, or if the host is a malformed
+   *           bracketed IPv6 address
    */
   public static HostPort valueOf(String hostPort) throws NumberFormatException,
           IllegalArgumentException
@@ -251,12 +252,18 @@
    * @throws NumberFormatException
    *           If the "port" in the supplied string cannot be converted to an int
    * @throws IllegalArgumentException
-   *           if no port could be found in the supplied string, or if the port
-   *           is not a valid port number
+   *           if no port could be found in the supplied string, if the port
+   *           is not a valid port number, or if the host is a malformed
+   *           bracketed IPv6 address
    */
   public static HostPort valueOf(String hostPort, Integer defaultPort) throws NumberFormatException,
       IllegalArgumentException
   {
+    if (hostPort.isEmpty())
+    {
+      throw new IllegalArgumentException(
+          "Invalid host/port string: no host name was provided in ''");
+    }
     final int sepIndex = hostPort.lastIndexOf(':');
     if ((hostPort.charAt(0) == '['
         && hostPort.charAt(hostPort.length() - 1) == ']')
@@ -276,8 +283,9 @@
           "Invalid host/port string: no host name was provided in '" + hostPort
               + "'");
     }
-    else if (hostPort.lastIndexOf(':', sepIndex - 1) != -1
-        && (hostPort.charAt(0) != '[' || hostPort.charAt(sepIndex - 1) != ']'))
+    else if (hostPort.charAt(0) == '['
+        ? hostPort.charAt(sepIndex - 1) != ']'
+        : hostPort.lastIndexOf(':', sepIndex - 1) != -1)
     {
       if (defaultPort != null)
       {
@@ -300,15 +308,24 @@
    * @param host
    *          the host name to clean
    * @return the cleaned up host name
+   * @throws IllegalArgumentException
+   *           if the host name starts or ends with a square bracket, but is
+   *           not a well-formed "[bracketed IPv6 address]"
    */
   private String removeExtraChars(String host)
   {
-    final int startsWith = host.indexOf("[");
-    if (startsWith == -1)
+    final boolean startsWithBracket = host.startsWith("[");
+    final boolean endsWithBracket = host.endsWith("]");
+    if (!startsWithBracket && !endsWithBracket)
     {
       return host;
     }
-    return host.substring(1, host.length() - 1);
+    if (startsWithBracket && endsWithBracket && host.length() > 2)
+    {
+      return host.substring(1, host.length() - 1);
+    }
+    throw new IllegalArgumentException(
+        "Invalid host name: expected \"[bracketed IPv6 address]\", but got '" + host + "'");
   }
 
   /**
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
index fac597f..9fbc728 100644
--- 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
@@ -53,4 +53,49 @@
   {
     Aci.decode(ByteString.valueOfUtf8(aciString), DN.rootDN());
   }
+
+  /**
+   * ACIs whose userdn LDAP URL contains a malformed bracketed IPv6 host - see
+   * issue #726.
+   *
+   * @return The malformed ACIs.
+   */
+  @DataProvider
+  public Object[][] malformedIPv6HostAcis()
+  {
+    return new Object[][] {
+      { "(version 3.0; acl \"f\"; allow (search) userdn=\"q://[:1\"; )" },
+      { "(version 3.0; acl \"f\"; allow (search) userdn=\"ldap://[::1:389/dc=example,dc=com\"; )" },
+    };
+  }
+
+  /**
+   * A malformed bracketed IPv6 host in the userdn URL must be rejected with a
+   * clean AciException instead of a StringIndexOutOfBoundsException - see
+   * issue #726.
+   *
+   * @param aciString
+   *          The ACI to decode.
+   * @throws Exception
+   *           If an unexpected exception occurred.
+   */
+  @Test(dataProvider = "malformedIPv6HostAcis", expectedExceptions = AciException.class)
+  public void testMalformedIPv6HostInUserDN(String aciString) throws Exception
+  {
+    Aci.decode(ByteString.valueOfUtf8(aciString), DN.rootDN());
+  }
+
+  /**
+   * A well-formed bracketed IPv6 host in the userdn URL must still decode.
+   *
+   * @throws Exception
+   *           If an unexpected exception occurred.
+   */
+  @Test
+  public void testValidIPv6HostInUserDN() throws Exception
+  {
+    String aciString = "(version 3.0; acl \"f\"; allow (search) "
+        + "userdn=\"ldap://[::1]:389/uid=user,dc=example,dc=com\"; )";
+    Aci.decode(ByteString.valueOfUtf8(aciString), DN.rootDN());
+  }
 }
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/types/HostPortTest.java b/opendj-server-legacy/src/test/java/org/opends/server/types/HostPortTest.java
index fe60d4b..87be1aa 100644
--- a/opendj-server-legacy/src/test/java/org/opends/server/types/HostPortTest.java
+++ b/opendj-server-legacy/src/test/java/org/opends/server/types/HostPortTest.java
@@ -12,6 +12,7 @@
  * information: "Portions Copyright [year] [name of copyright owner]".
  *
  * Copyright 2013-2016 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems LLC.
  */
 package org.opends.server.types;
 
@@ -210,6 +211,71 @@
     HostPort.valueOf("host:99999999");
   }
 
+  /** Issue #726: used to throw StringIndexOutOfBoundsException. */
+  @Test(expectedExceptions = IllegalArgumentException.class)
+  public void valueOfOpeningBracketOnlyHost()
+  {
+    HostPort.valueOf("[:1");
+  }
+
+  /** Issue #726: used to throw StringIndexOutOfBoundsException. */
+  @Test(expectedExceptions = IllegalArgumentException.class)
+  public void valueOfOpeningBracketOnlyHostDefaultPort()
+  {
+    HostPort.valueOf("[", 1);
+  }
+
+  @Test(expectedExceptions = IllegalArgumentException.class)
+  public void valueOfIPv6MissingClosingBracket()
+  {
+    HostPort.valueOf("[" + IPV6_ADDRESS + ":389");
+  }
+
+  @Test(expectedExceptions = IllegalArgumentException.class)
+  public void valueOfIPv6MissingOpeningBracket()
+  {
+    HostPort.valueOf(IPV6_ADDRESS + "]:389");
+  }
+
+  @Test(expectedExceptions = IllegalArgumentException.class)
+  public void valueOfEmptyBracketsHost()
+  {
+    HostPort.valueOf("[]:389");
+  }
+
+  @Test(expectedExceptions = IllegalArgumentException.class)
+  public void valueOfEmptyString()
+  {
+    HostPort.valueOf("");
+  }
+
+  /** Issue #726: used to throw StringIndexOutOfBoundsException. */
+  @Test(expectedExceptions = IllegalArgumentException.class)
+  public void constructorOpeningBracketOnlyHost()
+  {
+    new HostPort("[", 1);
+  }
+
+  @Test(expectedExceptions = IllegalArgumentException.class)
+  public void constructorMissingClosingBracket()
+  {
+    new HostPort("[" + IPV6_ADDRESS, 389);
+  }
+
+  @Test(expectedExceptions = IllegalArgumentException.class)
+  public void constructorMissingOpeningBracket()
+  {
+    new HostPort(IPV6_ADDRESS + "]", 389);
+  }
+
+  @Test
+  public void constructorBracketedIPv6()
+  {
+    final HostPort hp = new HostPort("[" + IPV6_ADDRESS + "]", 389);
+    assertThat(hp.getHost()).isEqualTo(IPV6_ADDRESS);
+    assertThat(hp.getPort()).isEqualTo(389);
+  }
+
   @Test
   public void valueOfIPv6NoPort()
   {

--
Gitblit v1.10.0