/*
|
* 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.protocols.jmx;
|
|
import static org.testng.Assert.*;
|
|
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayOutputStream;
|
import java.io.InvalidClassException;
|
import java.io.ObjectInputFilter;
|
import java.io.ObjectInputStream;
|
import java.io.ObjectOutputStream;
|
import java.util.Date;
|
import java.util.HashMap;
|
import java.util.Map;
|
|
|
import org.opends.server.DirectoryServerTestCase;
|
import org.testng.annotations.DataProvider;
|
import org.testng.annotations.Test;
|
|
/** Tests JMX RMI credential deserialization hardening. */
|
@Test(groups = { "precommit", "jmx" }, sequential = true)
|
public class RmiAuthenticatorTest extends DirectoryServerTestCase
|
{
|
/** Invalid credential shapes rejected before any bind attempt. */
|
@DataProvider(name = "invalidCredentials")
|
public Object[][] invalidCredentials()
|
{
|
return new Object[][]
|
{
|
{ null },
|
{ new Object[] { "cn=Directory Manager", "password" } },
|
{ new Date() },
|
{ new String[0] },
|
{ new String[] { "cn=Directory Manager" } },
|
{ new String[] { "cn=Directory Manager", "password", "extra" } },
|
{ new String[] { null, "password" } },
|
{ new String[] { "cn=Directory Manager", null } }
|
};
|
}
|
|
/** Verifies that RmiAuthenticator only accepts a two-element String array. */
|
@Test(dataProvider = "invalidCredentials", expectedExceptions = SecurityException.class)
|
public void rejectsInvalidCredentialShapes(Object credentials)
|
{
|
new RmiAuthenticator(null).authenticate(credentials);
|
}
|
|
/** Verifies that RMI connector environment constrains credential unmarshalling. */
|
@Test
|
public void configuresCredentialDeserializationProtection()
|
{
|
Map<String, Object> env = new HashMap<>();
|
RmiConnector.configureJmxDeserializationProtection(env);
|
|
assertEquals(env.get(RmiConnector.JMX_REMOTE_RMI_SERVER_CREDENTIALS_FILTER_PATTERN),
|
"maxdepth=3;maxarray=2;java.lang.String;!*");
|
// The connector-wide filter must NOT be set, so legitimate JMX traffic
|
// (MBean operations, notifications) is not affected by the allowlist.
|
assertNull(env.get("jmx.remote.rmi.server.serial.filter.pattern"));
|
// "jmx.remote.rmi.server.credential.types" is mutually exclusive with the
|
// credentials filter pattern: setting both prevents the connector from
|
// starting, so only the filter pattern must be configured.
|
assertNull(env.get("jmx.remote.rmi.server.credential.types"));
|
}
|
|
/** Verifies the configured filter allows only the expected credential payload. */
|
@Test
|
public void serialFilterAllowsOnlyTwoElementStringArray() throws Exception
|
{
|
Map<String, Object> env = new HashMap<>();
|
RmiConnector.configureJmxDeserializationProtection(env);
|
String filterPattern = (String) env.get(RmiConnector.JMX_REMOTE_RMI_SERVER_CREDENTIALS_FILTER_PATTERN);
|
|
assertEquals(readWithFilter(new String[] { "uid", "password" }, filterPattern),
|
new String[] { "uid", "password" });
|
assertRejectedByFilter(new Object[] { "uid", "password" }, filterPattern);
|
assertRejectedByFilter(new String[] { "uid", "password", "extra" }, filterPattern);
|
assertRejectedByFilter(new Date(), filterPattern);
|
}
|
|
private Object readWithFilter(Object object, String filterPattern) throws Exception
|
{
|
byte[] bytes = serialize(object);
|
try (ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes)))
|
{
|
in.setObjectInputFilter(ObjectInputFilter.Config.createFilter(filterPattern));
|
return in.readObject();
|
}
|
}
|
|
private byte[] serialize(Object object) throws Exception
|
{
|
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
try (ObjectOutputStream out = new ObjectOutputStream(bytes))
|
{
|
out.writeObject(object);
|
}
|
return bytes.toByteArray();
|
}
|
|
private void assertRejectedByFilter(Object object, String filterPattern) throws Exception
|
{
|
try
|
{
|
readWithFilter(object, filterPattern);
|
fail("Expected object to be rejected by the JMX credential serial filter");
|
}
|
catch (InvalidClassException expected)
|
{
|
// Expected: ObjectInputFilter rejected the class or array length.
|
}
|
}
|
}
|