mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

Valera V Harseko
yesterday 5fa8ed760437453c7694ce9e8bd76d21e2f5d683
CVE-2026-62375 Unbounded VLV offset array allocation leading to memory-exhaustion DoS
3 files modified
1 files added
177 ■■■■■ changed files
opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/EntryContainer.java 27 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/VLVIndex.java 8 ●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/controls/VLVRequestControl.java 16 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/controls/VLVOffsetAllocationDoSTest.java 126 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/EntryContainer.java
@@ -14,6 +14,7 @@
 * Copyright 2006-2010 Sun Microsystems, Inc.
 * Portions Copyright 2011-2016 ForgeRock AS.
 * Portions copyright 2013 Manuel Gaupp
 * Portions Copyright 2026 3A Systems, LLC
 */
package org.opends.server.backends.pluggable;
@@ -2688,21 +2689,29 @@
      afterCount = 0;
    }
    int count = 1 + beforeCount + afterCount;
    // Never allocate more longs than the number of entries actually available
    // from startPos, and compute the size with long arithmetic so an
    // attacker-supplied before/after count cannot overflow or drive an oversized
    // array (GHSA-q4wx-wj4j-4657).
    final int available = startPos >= 0 && startPos < sortMap.size() ? sortMap.size() - startPos : 0;
    final int count = (int) Math.max(0L, Math.min(1L + beforeCount + afterCount, available));
    long[] sortedIDs = new long[count];
    int treePos = 0;
    int arrayPos = 0;
    for (EntryID id : sortMap.values())
    if (count > 0)
    {
      if (treePos++ < startPos)
      for (EntryID id : sortMap.values())
      {
        continue;
      }
        if (treePos++ < startPos)
        {
          continue;
        }
      sortedIDs[arrayPos++] = id.longValue();
      if (arrayPos >= count)
      {
        break;
        sortedIDs[arrayPos++] = id.longValue();
        if (arrayPos >= count)
        {
          break;
        }
      }
    }
opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/VLVIndex.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2006-2008 Sun Microsystems, Inc.
 * Portions Copyright 2011-2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC
 */
package org.opends.server.backends.pluggable;
@@ -689,7 +690,12 @@
    }
    final long[] selectedIDs;
    final int count = 1 + beforeCount + afterCount;
    // Never allocate more longs than the number of entries actually available
    // from startPos, and compute the size with long arithmetic so an
    // attacker-supplied before/after count cannot overflow or drive an oversized
    // array (GHSA-q4wx-wj4j-4657).
    final int available = startPos >= 0 && startPos < currentCount ? currentCount - startPos : 0;
    final int count = (int) Math.max(0L, Math.min(1L + beforeCount + afterCount, available));
    try (Cursor<ByteString, ByteString> cursor = txn.openCursor(getName()))
    {
      if (cursor.positionToIndex(startPos))
opendj-server-legacy/src/main/java/org/opends/server/controls/VLVRequestControl.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2008 Sun Microsystems, Inc.
 * Portions Copyright 2014-2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC
 */
package org.opends.server.controls;
import org.forgerock.i18n.LocalizableMessage;
@@ -74,6 +75,16 @@
        int beforeCount = (int)reader.readInteger();
        int afterCount  = (int)reader.readInteger();
        // The VLV draft defines beforeCount/afterCount as INTEGER (0..maxInt).
        // Reject negative values (including those produced by the (int) cast
        // wrapping a wire value above 2^31-1): unchecked they would flow into an
        // unbounded / overflowing array allocation downstream (GHSA-q4wx-wj4j-4657).
        if (beforeCount < 0 || afterCount < 0)
        {
          throw new DirectoryException(ResultCode.PROTOCOL_ERROR,
              INFO_VLVREQ_CONTROL_CANNOT_DECODE_VALUE.get("beforeCount and afterCount must not be negative"));
        }
        int offset = 0;
        int contentCount = 0;
        ByteString greaterThanOrEqual = null;
@@ -85,6 +96,11 @@
            offset = (int)reader.readInteger();
            contentCount = (int)reader.readInteger();
            reader.readEndSequence();
            if (offset < 0 || contentCount < 0)
            {
              throw new DirectoryException(ResultCode.PROTOCOL_ERROR,
                  INFO_VLVREQ_CONTROL_CANNOT_DECODE_VALUE.get("offset and contentCount must not be negative"));
            }
            break;
          case TYPE_TARGET_GREATERTHANOREQUAL:
opendj-server-legacy/src/test/java/org/opends/server/controls/VLVOffsetAllocationDoSTest.java
New file
@@ -0,0 +1,126 @@
/*
 * 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.controls;
import static org.opends.server.protocols.internal.InternalClientConnection.*;
import static org.opends.server.protocols.internal.Requests.*;
import static org.testng.Assert.*;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.SearchScope;
import org.opends.server.TestCaseUtils;
import org.opends.server.protocols.internal.InternalSearchOperation;
import org.opends.server.protocols.internal.SearchRequest;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
/**
 * Regression test for GHSA-q4wx-wj4j-4657 / OPENDJ-003 — unbounded array
 * allocation via VLV beforeCount/afterCount/offset (CWE-789 / CWE-770 / CWE-190).
 * <p>
 * A VLV-by-offset request carries attacker-controlled before/after counts. The
 * server computed {@code count = 1 + beforeCount + afterCount} and allocated
 * {@code new long[count]} without clamping it to the actual list size. With
 * {@code afterCount = Integer.MAX_VALUE} the sum overflowed to a negative int,
 * yielding a {@code NegativeArraySizeException} (or, with large non-overflowing
 * values, a multi-gigabyte allocation / {@code OutOfMemoryError}) from a single
 * search request. The fix computes the count with long arithmetic and clamps it
 * to the number of entries actually available, so the oversized window is now
 * bounded instead of faulting the search handler.
 * <p>
 * Note: this exercises the default in-memory sort path
 * ({@code EntryContainer.sortByOffset}), which requires no VLV index — the same
 * unclamped allocation also existed in {@code VLVIndex.readRange}.
 */
@SuppressWarnings("javadoc")
public class VLVOffsetAllocationDoSTest extends ControlsTestCase
{
  @BeforeClass
  public void startServer() throws Exception
  {
    TestCaseUtils.startServer();
  }
  private void populateDB() throws Exception
  {
    TestCaseUtils.clearBackend("userRoot", "dc=example,dc=com");
    TestCaseUtils.addEntries(
        "dn: uid=albert.zimmerman,dc=example,dc=com",
        "objectClass: top",
        "objectClass: person",
        "objectClass: organizationalPerson",
        "objectClass: inetOrgPerson",
        "uid: albert.zimmerman",
        "givenName: Albert",
        "sn: Zimmerman",
        "cn: Albert Zimmerman",
        "",
        "dn: uid=aaron.zimmerman,dc=example,dc=com",
        "objectClass: top",
        "objectClass: person",
        "objectClass: organizationalPerson",
        "objectClass: inetOrgPerson",
        "uid: aaron.zimmerman",
        "givenName: Aaron",
        "sn: Zimmerman",
        "cn: Aaron Zimmerman",
        "",
        "dn: uid=mary.jones,dc=example,dc=com",
        "objectClass: top",
        "objectClass: person",
        "objectClass: organizationalPerson",
        "objectClass: inetOrgPerson",
        "uid: mary.jones",
        "givenName: Mary",
        "sn: Jones",
        "cn: Mary Jones");
  }
  /**
   * A single VLV-by-offset search with {@code afterCount = Integer.MAX_VALUE}
   * (which previously overflowed {@code 1 + beforeCount + afterCount} into a
   * negative array size) must now be clamped to the list size and return the
   * same bounded page as a normal request — not fault the search handler with a
   * {@code NegativeArraySizeException} / {@code OTHER}.
   */
  @Test
  public void offsetAllocationOverflowIsClamped() throws Exception
  {
    populateDB();
    // Baseline: a normal, bounded window.
    SearchRequest ok = newSearchRequest("dc=example,dc=com", SearchScope.WHOLE_SUBTREE, "(objectClass=person)")
        .addControl(new ServerSideSortRequestControl("givenName"))
        .addControl(new VLVRequestControl(0, 3, 1, 0));
    InternalSearchOperation okOp = getRootConnection().processSearch(ok);
    assertEquals(okOp.getResultCode(), ResultCode.SUCCESS);
    // Oversized window: afterCount = Integer.MAX_VALUE. Before the fix this
    // overflowed to new long[-2147483648]; a larger non-overflowing count would
    // instead force a multi-gigabyte allocation (OutOfMemoryError).
    SearchRequest bad = newSearchRequest("dc=example,dc=com", SearchScope.WHOLE_SUBTREE, "(objectClass=person)")
        .addControl(new ServerSideSortRequestControl("givenName"))
        .addControl(new VLVRequestControl(0, Integer.MAX_VALUE, 1, 0));
    InternalSearchOperation badOp = getRootConnection().processSearch(bad);
    assertEquals(badOp.getResultCode(), ResultCode.SUCCESS,
        "oversized VLV afterCount must be clamped to the list size, not fault the search handler "
            + "(GHSA-q4wx-wj4j-4657): " + badOp.getErrorMessage());
    // The page is bounded by the number of entries that actually exist (offset 1
    // onwards), i.e. the same result as the bounded baseline request.
    assertEquals(badOp.getSearchEntries().size(), okOp.getSearchEntries().size());
  }
}