/*
|
* 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());
|
}
|
}
|