This is an automated email from the ASF dual-hosted git repository.

ppkarwasz pushed a commit to branch feat/2.x/filtered-stream-reject-proxies
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git

commit 13b4b03f9e262a4238ebbc7ad62ffc6a796d0add
Author: Piotr P. Karwasz <[email protected]>
AuthorDate: Wed Aug 26 23:49:03 2026 +0200

    Reject dynamic proxies in FilteredObjectInputStream
    
    Dynamic proxy class descriptors are resolved through
    `resolveProxyClass`, not `resolveClass`, so they bypassed the
    deserialization allowlist on Java 8. The JEP 290 filter used on
    Java 9 and later already rejects proxies, since their synthetic class
    names never match the allowlist; this aligns the Java 8 path by
    rejecting proxies outright.
    
    Part of the hardening series from #4168.
    
    Assisted-By: Claude Fable 5 <[email protected]>
---
 .../log4j/util/FilteredObjectInputStreamTest.java  | 91 ++++++++++++++++++++++
 .../log4j/util/FilteredObjectInputStream.java      | 14 ++++
 .../.2.x.x/reject_proxy_deserialization.xml        | 11 +++
 3 files changed, 116 insertions(+)

diff --git 
a/log4j-api-test/src/test/java/org/apache/logging/log4j/util/FilteredObjectInputStreamTest.java
 
b/log4j-api-test/src/test/java/org/apache/logging/log4j/util/FilteredObjectInputStreamTest.java
new file mode 100644
index 0000000000..5eece13827
--- /dev/null
+++ 
b/log4j-api-test/src/test/java/org/apache/logging/log4j/util/FilteredObjectInputStreamTest.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.logging.log4j.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.InvalidObjectException;
+import java.io.Serializable;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.logging.log4j.test.junit.SerialUtil;
+import org.junit.jupiter.api.Test;
+
+class FilteredObjectInputStreamTest {
+
+    private static FilteredObjectInputStream filteredStream(final byte[] data) 
throws Exception {
+        return new FilteredObjectInputStream(new ByteArrayInputStream(data));
+    }
+
+    @Test
+    void allow_listed_object_graph_round_trips() throws Exception {
+        final Map<String, Object> original = new HashMap<>();
+        original.put("string", "value");
+        original.put("integer", 17);
+        original.put("array", new int[] {1, 2, 3});
+        final Object restored =
+                filteredStream(SerialUtil.serialize((Serializable) 
original)).readObject();
+        assertEquals(original.keySet(), ((Map<?, ?>) restored).keySet());
+    }
+
+    @Test
+    void class_outside_allow_list_is_rejected() throws Exception {
+        final byte[] data = SerialUtil.serialize(new File("rejected"));
+        assertThrows(InvalidObjectException.class, () -> 
filteredStream(data).readObject());
+    }
+
+    @Test
+    void dynamic_proxy_is_rejected() throws Exception {
+        final byte[] data = SerialUtil.serialize((Serializable) 
serializableProxy());
+        assertThrows(InvalidObjectException.class, () -> 
filteredStream(data).readObject());
+    }
+
+    @Test
+    void allowed_extra_classes_do_not_re_enable_proxies() throws Exception {
+        final Object proxy = serializableProxy();
+        final byte[] data = SerialUtil.serialize((Serializable) proxy);
+        final FilteredObjectInputStream stream = new FilteredObjectInputStream(
+                new ByteArrayInputStream(data),
+                Collections.singleton(proxy.getClass().getName()));
+        assertThrows(InvalidObjectException.class, stream::readObject);
+    }
+
+    private static Object serializableProxy() {
+        return Proxy.newProxyInstance(
+                FilteredObjectInputStreamTest.class.getClassLoader(),
+                new Class<?>[] {Comparator.class},
+                new SerializableInvocationHandler());
+    }
+
+    private static class SerializableInvocationHandler implements 
InvocationHandler, Serializable {
+
+        private static final long serialVersionUID = 1L;
+
+        @Override
+        public Object invoke(final Object proxy, final Method method, final 
Object[] args) {
+            return null;
+        }
+    }
+}
diff --git 
a/log4j-api/src/main/java/org/apache/logging/log4j/util/FilteredObjectInputStream.java
 
b/log4j-api/src/main/java/org/apache/logging/log4j/util/FilteredObjectInputStream.java
index d207e41949..5ed82585ea 100644
--- 
a/log4j-api/src/main/java/org/apache/logging/log4j/util/FilteredObjectInputStream.java
+++ 
b/log4j-api/src/main/java/org/apache/logging/log4j/util/FilteredObjectInputStream.java
@@ -71,6 +71,20 @@ public class FilteredObjectInputStream extends 
ObjectInputStream {
         return super.resolveClass(desc);
     }
 
+    /**
+     * Unconditionally rejects dynamic proxy classes.
+     * <p>
+     *     Proxy class descriptors do not pass through {@link 
#resolveClass(ObjectStreamClass)}, so they would
+     *     otherwise bypass the allowlist entirely. No supported Log4j 
serialized form contains a dynamic proxy, and
+     *     the JEP 290 filter used on Java 9 and later rejects proxy classes 
as well, since their synthetic class names
+     *     never match the allowlist.
+     * </p>
+     */
+    @Override
+    protected Class<?> resolveProxyClass(final String[] interfaces) throws 
IOException, ClassNotFoundException {
+        throw new InvalidObjectException("Proxy classes are not allowed for 
deserialization");
+    }
+
     private static boolean isAllowedByDefault(final String name) {
         return isRequiredPackage(name) || REQUIRED_JAVA_CLASSES.contains(name);
     }
diff --git a/src/changelog/.2.x.x/reject_proxy_deserialization.xml 
b/src/changelog/.2.x.x/reject_proxy_deserialization.xml
new file mode 100644
index 0000000000..0af534b693
--- /dev/null
+++ b/src/changelog/.2.x.x/reject_proxy_deserialization.xml
@@ -0,0 +1,11 @@
+<?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">
+  <description format="asciidoc">
+    `FilteredObjectInputStream` now rejects dynamic proxy classes.
+  </description>
+</entry>

Reply via email to