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>
