/*
|
* 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.dsml.protocol;
|
|
import java.io.ByteArrayInputStream;
|
import java.io.IOException;
|
import java.net.InetAddress;
|
import java.net.URI;
|
|
import org.forgerock.opendj.ldap.ByteString;
|
import org.forgerock.testng.ForgeRockTestCase;
|
import org.testng.annotations.AfterMethod;
|
import org.testng.annotations.DataProvider;
|
import org.testng.annotations.Test;
|
|
import static org.testng.Assert.assertEquals;
|
import static org.testng.Assert.assertFalse;
|
import static org.testng.Assert.assertTrue;
|
|
/**
|
* Tests the anyURI dereferencing hardening added for GHSA-68r5-9hpg-7qw9:
|
* secure-by-default (no fetch), scheme allowlist, internal-address filter,
|
* redirect refusal and content size cap. None of these tests performs network
|
* or DNS I/O: hosts are IP literals and streams are in-memory.
|
*/
|
@SuppressWarnings("javadoc")
|
@Test(groups = { "precommit", "dsml" })
|
public class ByteStringUtilityTestCase extends ForgeRockTestCase
|
{
|
@AfterMethod
|
public void restoreDefaults()
|
{
|
ByteStringUtility.setDereferenceUri(false);
|
ByteStringUtility.setAllowedUriSchemes("http,https");
|
ByteStringUtility.setMaxUriContentLength(10L * 1024 * 1024);
|
}
|
|
@Test
|
public void testUriIsStoredVerbatimByDefault() throws Exception
|
{
|
ByteString value = ByteStringUtility.convertValue(new URI("http://203.0.113.10/secret"));
|
assertEquals(value, ByteString.valueOfUtf8("http://203.0.113.10/secret"));
|
}
|
|
@DataProvider
|
public Object[][] disallowedSchemeUris()
|
{
|
return new Object[][] {
|
{ "file:///etc/passwd" },
|
{ "ftp://203.0.113.10/file" },
|
{ "gopher://203.0.113.10/1" },
|
{ "jar:http://203.0.113.10/a.jar!/b" },
|
{ "urn:isbn:0451450523" }, // opaque URI, no scheme match
|
{ "/relative/path" }, // no scheme at all
|
};
|
}
|
|
@Test(dataProvider = "disallowedSchemeUris", expectedExceptions = IOException.class,
|
expectedExceptionsMessageRegExp = ".*disallowed scheme.*")
|
public void testDisallowedSchemesAreRejected(String uri) throws Exception
|
{
|
ByteStringUtility.setDereferenceUri(true);
|
ByteStringUtility.convertUri(new URI(uri));
|
}
|
|
@Test(expectedExceptions = IOException.class,
|
expectedExceptionsMessageRegExp = ".*disallowed scheme.*")
|
public void testSchemeAllowlistIsConfigurable() throws Exception
|
{
|
ByteStringUtility.setDereferenceUri(true);
|
ByteStringUtility.setAllowedUriSchemes(" HTTPS , ");
|
// http is no longer on the allowlist, so it is rejected before any I/O.
|
ByteStringUtility.convertUri(new URI("http://203.0.113.10/"));
|
}
|
|
@DataProvider
|
public Object[][] internalUris()
|
{
|
return new Object[][] {
|
{ "http://127.0.0.1/" },
|
{ "http://0.0.0.0/" },
|
{ "http://10.1.2.3/" },
|
{ "http://172.16.5.5/" },
|
{ "http://192.168.1.1/" },
|
{ "http://169.254.169.254/latest/meta-data/" },
|
{ "http://100.100.100.200/latest/meta-data/" },
|
{ "http://198.18.0.1/" },
|
{ "http://240.0.0.1/" },
|
{ "http://[::1]/" },
|
{ "http://[fe80::1]/" },
|
{ "http://[fc00::1]/" },
|
{ "http://[fd12:3456::1]/" },
|
};
|
}
|
|
@Test(dataProvider = "internalUris", expectedExceptions = IOException.class,
|
expectedExceptionsMessageRegExp = ".*(internal|no host).*")
|
public void testInternalAddressesAreRejected(String uri) throws Exception
|
{
|
ByteStringUtility.assertHostAllowed(new URI(uri));
|
}
|
|
@Test(expectedExceptions = IOException.class,
|
expectedExceptionsMessageRegExp = ".*no host.*")
|
public void testUriWithoutHostIsRejected() throws Exception
|
{
|
ByteStringUtility.assertHostAllowed(new URI("http:///path"));
|
}
|
|
@DataProvider
|
public Object[][] addressClassification()
|
{
|
return new Object[][] {
|
// address, expected isInternalAddress
|
{ "8.8.8.8", false },
|
{ "1.1.1.1", false },
|
{ "203.0.113.10", false },
|
{ "2001:4860:4860::8888", false },
|
{ "127.0.0.1", true },
|
{ "0.1.2.3", true },
|
{ "169.254.169.254", true },
|
{ "239.255.255.255", true }, // multicast
|
{ "255.255.255.255", true }, // broadcast
|
// 100.64.0.0/10 boundaries
|
{ "100.63.255.255", false },
|
{ "100.64.0.0", true },
|
{ "100.100.100.200", true },
|
{ "100.127.255.255", true },
|
{ "100.128.0.0", false },
|
// 192.0.0.0/24 boundaries
|
{ "192.0.0.1", true },
|
{ "192.0.1.1", false },
|
// 198.18.0.0/15 boundaries
|
{ "198.17.255.255", false },
|
{ "198.18.0.0", true },
|
{ "198.19.255.255", true },
|
{ "198.20.0.0", false },
|
// 240.0.0.0/4 boundary
|
{ "239.0.0.1", true }, // multicast anyway
|
{ "240.0.0.0", true },
|
// IPv6 unique-local fc00::/7 boundaries
|
{ "fbff::1", false },
|
{ "fc00::1", true },
|
{ "fdff::1", true },
|
{ "fe00::1", false },
|
};
|
}
|
|
@Test(dataProvider = "addressClassification")
|
public void testIsInternalAddress(String literal, boolean expectedInternal) throws Exception
|
{
|
// IP literals do not trigger DNS resolution.
|
InetAddress address = InetAddress.getByName(literal);
|
assertEquals(ByteStringUtility.isInternalAddress(address), expectedInternal,
|
"isInternalAddress(" + literal + ")");
|
}
|
|
@DataProvider
|
public Object[][] redirectCodes()
|
{
|
return new Object[][] { { 300 }, { 301 }, { 302 }, { 303 }, { 307 }, { 308 } };
|
}
|
|
@Test(dataProvider = "redirectCodes", expectedExceptions = IOException.class,
|
expectedExceptionsMessageRegExp = ".*redirect.*")
|
public void testRedirectsAreRefused(int responseCode) throws Exception
|
{
|
ByteStringUtility.assertNotRedirect(responseCode, new URI("http://203.0.113.10/"));
|
}
|
|
@Test
|
public void testNonRedirectCodesAreAccepted() throws Exception
|
{
|
ByteStringUtility.assertNotRedirect(200, new URI("http://203.0.113.10/"));
|
ByteStringUtility.assertNotRedirect(404, new URI("http://203.0.113.10/"));
|
}
|
|
@Test
|
public void testContentWithinCapIsReadFully() throws Exception
|
{
|
byte[] content = new byte[8192];
|
for (int i = 0; i < content.length; i++)
|
{
|
content[i] = (byte) i;
|
}
|
ByteStringUtility.setMaxUriContentLength(content.length);
|
ByteString value = ByteStringUtility.readCapped(
|
new ByteArrayInputStream(content), new URI("http://203.0.113.10/"));
|
assertEquals(value, ByteString.wrap(content));
|
}
|
|
@Test(expectedExceptions = IOException.class,
|
expectedExceptionsMessageRegExp = ".*maximum allowed size.*")
|
public void testContentOverCapIsRejected() throws Exception
|
{
|
ByteStringUtility.setMaxUriContentLength(1024);
|
ByteStringUtility.readCapped(
|
new ByteArrayInputStream(new byte[1025]), new URI("http://203.0.113.10/"));
|
}
|
|
@Test(expectedExceptions = IllegalArgumentException.class)
|
public void testZeroMaxSizeIsRejected()
|
{
|
ByteStringUtility.setMaxUriContentLength(0);
|
}
|
|
@Test(expectedExceptions = IllegalArgumentException.class)
|
public void testNegativeMaxSizeIsRejected()
|
{
|
ByteStringUtility.setMaxUriContentLength(-1);
|
}
|
|
@Test
|
public void testSchemeCheckPrecedesHostCheck() throws Exception
|
{
|
ByteStringUtility.setDereferenceUri(true);
|
// A disallowed scheme must be rejected even for an internal host,
|
// and the internal host must be rejected for an allowed scheme.
|
try
|
{
|
ByteStringUtility.convertUri(new URI("file://127.0.0.1/etc/passwd"));
|
assertFalse(true, "expected IOException for disallowed scheme");
|
}
|
catch (IOException e)
|
{
|
assertTrue(e.getMessage().contains("disallowed scheme"), e.getMessage());
|
}
|
try
|
{
|
ByteStringUtility.convertUri(new URI("http://127.0.0.1/"));
|
assertFalse(true, "expected IOException for internal address");
|
}
|
catch (IOException e)
|
{
|
assertTrue(e.getMessage().contains("internal"), e.getMessage());
|
}
|
}
|
}
|