Apply the deserialization filter in test helpers on Java 9+

The `SerialUtil` test helper returned a plain, unfiltered
`ObjectInputStream` on Java 9 and later, so the deserialization
allowlist was only ever exercised by tests running on a Java 8
toolchain. It now installs `DefaultObjectInputFilter` (reflectively,
since the class must still compile and run on Java 8), combined with an
`ObjectInputFilter.Config.createFilter` delegate built from the
caller-supplied extra allowed classes, reproducing the
`FilteredObjectInputStream` semantics on modern JDKs. When
`DefaultObjectInputFilter` is absent (it only exists in the packaged
multi-release JAR), the helper falls back to
`FilteredObjectInputStream` on any Java version.

`SerializationTestHelper` in `log4j-1.2-api` now delegates to
`SerialUtil`, so its `org.apache.log4j.*` extras are honored on all
Java versions as well.

Part of the hardening series from #4168.

Assisted-By: Claude Fable 5 <noreply@anthropic.com>
diff --git a/log4j-1.2-api/src/test/java/org/apache/log4j/util/SerializationTestHelper.java b/log4j-1.2-api/src/test/java/org/apache/log4j/util/SerializationTestHelper.java
index ab9b497..ba97511 100644
--- a/log4j-1.2-api/src/test/java/org/apache/log4j/util/SerializationTestHelper.java
+++ b/log4j-1.2-api/src/test/java/org/apache/log4j/util/SerializationTestHelper.java
@@ -30,8 +30,7 @@
 import java.util.Arrays;
 import java.util.Collection;
 import org.apache.commons.io.FileUtils;
-import org.apache.logging.log4j.util.Constants;
-import org.apache.logging.log4j.util.FilteredObjectInputStream;
+import org.apache.logging.log4j.test.junit.SerialUtil;
 
 /**
  * Utiities for serialization tests.
@@ -114,15 +113,12 @@
     }
 
     private static ObjectInputStream newObjectInputStream(final InputStream in) throws IOException {
-        if (Constants.JAVA_MAJOR_VERSION == 8) {
-            // FilteredObjectInputStream's default allow-list covers `org.apache.logging.log4j.` but
-            // not the `org.apache.log4j.` 1.2-compatibility namespace, so we have to enumerate the
-            // 1.2 classes that the tests in this module deserialize on Java 8.
-            final Collection<String> allowedLog4j12Classes =
-                    Arrays.asList("org.apache.log4j.Level", "org.apache.log4j.LevelTest$CustomLevel");
-            return new FilteredObjectInputStream(in, allowedLog4j12Classes);
-        }
-        return new ObjectInputStream(in);
+        // The default allow-list covers `org.apache.logging.log4j.` but
+        // not the `org.apache.log4j.` 1.2-compatibility namespace, so we have to enumerate the
+        // 1.2 classes that the tests in this module deserialize.
+        final Collection<String> allowedLog4j12Classes =
+                Arrays.asList("org.apache.log4j.Level", "org.apache.log4j.LevelTest$CustomLevel");
+        return SerialUtil.getObjectInputStream(in, allowedLog4j12Classes);
     }
 
     /**
diff --git a/log4j-api-test/src/main/java/org/apache/logging/log4j/test/SerializableMatchers.java b/log4j-api-test/src/main/java/org/apache/logging/log4j/test/SerializableMatchers.java
index a5af542..2aff5fe 100644
--- a/log4j-api-test/src/main/java/org/apache/logging/log4j/test/SerializableMatchers.java
+++ b/log4j-api-test/src/main/java/org/apache/logging/log4j/test/SerializableMatchers.java
@@ -39,7 +39,7 @@
 
     /**
      * Same as {@link #serializesRoundTrip(Matcher)} but extends the default deserialization
-     * allow-list on Java 8 (see {@link SerialUtil#deserialize(byte[], Collection)}).
+     * allowlist (see {@link SerialUtil#deserialize(byte[], Collection)}).
      */
     public static <T extends Serializable> Matcher<T> serializesRoundTrip(
             final Matcher<T> matcher, final Collection<String> allowedExtraClasses) {
@@ -64,8 +64,8 @@
     }
 
     /**
-     * Same as {@link #serializesRoundTrip()} but extends the default deserialization allow-list on
-     * Java 8 (see {@link SerialUtil#deserialize(byte[], Collection)}).
+     * Same as {@link #serializesRoundTrip()} but extends the default deserialization allow-list
+     * (see {@link SerialUtil#deserialize(byte[], Collection)}).
      */
     public static Matcher<? super Serializable> serializesRoundTrip(final Collection<String> allowedExtraClasses) {
         return serializesRoundTrip(any(Serializable.class), allowedExtraClasses);
diff --git a/log4j-api-test/src/main/java/org/apache/logging/log4j/test/junit/SerialUtil.java b/log4j-api-test/src/main/java/org/apache/logging/log4j/test/junit/SerialUtil.java
index 34600f0..89e7839 100644
--- a/log4j-api-test/src/main/java/org/apache/logging/log4j/test/junit/SerialUtil.java
+++ b/log4j-api-test/src/main/java/org/apache/logging/log4j/test/junit/SerialUtil.java
@@ -24,6 +24,7 @@
 import java.io.ObjectOutput;
 import java.io.ObjectOutputStream;
 import java.io.Serializable;
+import java.lang.reflect.Method;
 import java.util.Collection;
 import java.util.Collections;
 import org.apache.logging.log4j.test.internal.annotation.SuppressFBWarnings;
@@ -35,6 +36,34 @@
  */
 public class SerialUtil {
 
+    // On Java 9+ streams are filtered with `DefaultObjectInputFilter`, which must be accessed reflectively.
+    private static final Method createFilter;
+    private static final Method newDefaultObjectInputFilter;
+    private static final Method setObjectInputFilter;
+
+    static {
+        Method createFilterMethod = null;
+        Method newInstanceMethod = null;
+        Method setFilterMethod = null;
+        if (Constants.JAVA_MAJOR_VERSION != 8) {
+            try {
+                final Class<?> filterClass = Class.forName("java.io.ObjectInputFilter");
+                createFilterMethod =
+                        Class.forName("java.io.ObjectInputFilter$Config").getMethod("createFilter", String.class);
+                newInstanceMethod = Class.forName("org.apache.logging.log4j.util.internal.DefaultObjectInputFilter")
+                        .getMethod("newInstance", filterClass);
+                setFilterMethod = ObjectInputStream.class.getMethod("setObjectInputFilter", filterClass);
+            } catch (final ReflectiveOperationException e) {
+                createFilterMethod = null;
+                newInstanceMethod = null;
+                // setFilterMethod is already null
+            }
+        }
+        createFilter = createFilterMethod;
+        newDefaultObjectInputFilter = newInstanceMethod;
+        setObjectInputFilter = setFilterMethod;
+    }
+
     private SerialUtil() {}
 
     /**
@@ -76,12 +105,10 @@
     }
 
     /**
-     * Deserialize an object from the specified byte array using a {@link FilteredObjectInputStream}
-     * extended with the supplied allow-list (Java 8 only — Java 9+ uses the JVM's serialization
-     * filter, so the allow-list is ignored).
+     * Deserialize an object from the specified byte array using a stream that applies Log4j's
+     * deserialization allow-list, extended with the supplied extra classes.
      * @param data byte array representing the serialized object
-     * @param allowedExtraClasses fully-qualified class names to add to {@link
-     *     FilteredObjectInputStream}'s default allow-list on Java 8
+     * @param allowedExtraClasses fully-qualified class names to add to the default allow-list
      * @return the deserialized object
      */
     @SuppressWarnings("unchecked")
@@ -106,8 +133,8 @@
     }
 
     /**
-     * Creates an {@link ObjectInputStream} adapted to the current Java version, extended with the
-     * supplied allow-list on Java 8.
+     * Creates an {@link ObjectInputStream} adapted to the current Java version, applying Log4j's
+     * deserialization allow-list extended with the supplied extra classes.
      */
     @SuppressFBWarnings("OBJECT_DESERIALIZATION")
     public static ObjectInputStream getObjectInputStream(
@@ -127,14 +154,24 @@
     }
 
     /**
-     * Creates an {@link ObjectInputStream} adapted to the current Java version, extended with the
-     * supplied allow-list on Java 8.
+     * Creates an {@link ObjectInputStream} adapted to the current Java version, applying Log4j's
+     * deserialization allowlist extended with the supplied extra classes.
      */
     @SuppressFBWarnings("OBJECT_DESERIALIZATION")
     public static ObjectInputStream getObjectInputStream(
             final InputStream stream, final Collection<String> allowedExtraClasses) throws IOException {
-        return Constants.JAVA_MAJOR_VERSION == 8
-                ? new FilteredObjectInputStream(stream, allowedExtraClasses)
-                : new ObjectInputStream(stream);
+        if (Constants.JAVA_MAJOR_VERSION == 8 || newDefaultObjectInputFilter == null) {
+            return new FilteredObjectInputStream(stream, allowedExtraClasses);
+        }
+        final ObjectInputStream ois = new ObjectInputStream(stream);
+        try {
+            final Object extraClassesFilter = allowedExtraClasses.isEmpty()
+                    ? null
+                    : createFilter.invoke(null, String.join(";", allowedExtraClasses));
+            setObjectInputFilter.invoke(ois, newDefaultObjectInputFilter.invoke(null, extraClassesFilter));
+        } catch (final ReflectiveOperationException e) {
+            throw new IllegalStateException("Unable to install the deserialization filter", e);
+        }
+        return ois;
     }
 }
diff --git a/src/changelog/.2.x.x/4270_enforce_test_deserialization_filter_java9.xml b/src/changelog/.2.x.x/4270_enforce_test_deserialization_filter_java9.xml
new file mode 100644
index 0000000..d17cbaa
--- /dev/null
+++ b/src/changelog/.2.x.x/4270_enforce_test_deserialization_filter_java9.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<entry xmlns="https://logging.apache.org/xml/ns"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xsi:schemaLocation="
+           https://logging.apache.org/xml/ns
+           https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
+       type="changed">
+  <issue id="4270" link="https://github.com/apache/logging-log4j2/pull/4270"/>
+  <description format="asciidoc">
+    The `SerialUtil` test helper now applies the deserialization allowlist also on Java 9 and later.
+  </description>
+</entry>