/* * 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 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 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. } } }