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

markt-asf pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tomcat.git


The following commit(s) were added to refs/heads/main by this push:
     new 986d7fa55e Extend filtering to include dynamic proxy interface classes
986d7fa55e is described below

commit 986d7fa55e533f82cca92e4c72b62dbca2f8aee2
Author: Mark Thomas <[email protected]>
AuthorDate: Thu Jul 9 11:54:20 2026 +0100

    Extend filtering to include dynamic proxy interface classes
---
 .../catalina/util/CustomObjectInputStream.java     |  37 ++++---
 .../catalina/util/TestCustomObjectInputStream.java | 115 +++++++++++++++++++++
 webapps/docs/changelog.xml                         |   4 +
 3 files changed, 143 insertions(+), 13 deletions(-)

diff --git a/java/org/apache/catalina/util/CustomObjectInputStream.java 
b/java/org/apache/catalina/util/CustomObjectInputStream.java
index f20337dad2..80f869b997 100644
--- a/java/org/apache/catalina/util/CustomObjectInputStream.java
+++ b/java/org/apache/catalina/util/CustomObjectInputStream.java
@@ -128,19 +128,7 @@ public final class CustomObjectInputStream extends 
ObjectInputStream {
     public Class<?> resolveClass(ObjectStreamClass classDesc) throws 
ClassNotFoundException, IOException {
 
         String name = classDesc.getName();
-        if (allowedClassNamePattern != null) {
-            boolean allowed = allowedClassNamePattern.matcher(name).matches();
-            if (!allowed) {
-                boolean doLog = warnOnFailure && reportedClasses.add(name);
-                String msg = sm.getString("customObjectInputStream.nomatch", 
name, allowedClassNameFilter);
-                if (doLog) {
-                    log.warn(msg);
-                } else if (log.isDebugEnabled()) {
-                    log.debug(msg);
-                }
-                throw new InvalidClassException(msg);
-            }
-        }
+        checkAllowed(name);
 
         try {
             return Class.forName(name, false, classLoader);
@@ -166,6 +154,7 @@ public final class CustomObjectInputStream extends 
ObjectInputStream {
 
         Class<?>[] cinterfaces = new Class[interfaces.length];
         for (int i = 0; i < interfaces.length; i++) {
+            checkAllowed(interfaces[i]);
             cinterfaces[i] = classLoader.loadClass(interfaces[i]);
         }
 
@@ -178,4 +167,26 @@ public final class CustomObjectInputStream extends 
ObjectInputStream {
             throw new ClassNotFoundException(null, e);
         }
     }
+
+
+    /**
+     * Check the given class name against the configured filter, throwing an 
exception if it is not permitted. If no
+     * filter has been configured, all class names are permitted.
+     *
+     * @param name The fully qualified class name to check
+     *
+     * @throws InvalidClassException if the class name is not permitted by the 
configured filter
+     */
+    private void checkAllowed(String name) throws InvalidClassException {
+        if (allowedClassNamePattern != null && 
!allowedClassNamePattern.matcher(name).matches()) {
+            boolean doLog = warnOnFailure && reportedClasses.add(name);
+            String msg = sm.getString("customObjectInputStream.nomatch", name, 
allowedClassNameFilter);
+            if (doLog) {
+                log.warn(msg);
+            } else if (log.isDebugEnabled()) {
+                log.debug(msg);
+            }
+            throw new InvalidClassException(msg);
+        }
+    }
 }
diff --git a/test/org/apache/catalina/util/TestCustomObjectInputStream.java 
b/test/org/apache/catalina/util/TestCustomObjectInputStream.java
new file mode 100644
index 0000000000..7f154b63a1
--- /dev/null
+++ b/test/org/apache/catalina/util/TestCustomObjectInputStream.java
@@ -0,0 +1,115 @@
+/*
+ * 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.catalina.util;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InvalidClassException;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.regex.Pattern;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import org.apache.juli.logging.LogFactory;
+
+public class TestCustomObjectInputStream {
+
+    /*
+     * A minimal, serializable {@link InvocationHandler} so that a dynamic 
proxy instance can be serialized and then
+     * deserialized. Its class lives in {@code org.apache.catalina.util}, so 
it is permitted by the allow pattern used
+     * in the test below; only the proxied interface is not permitted.
+     */
+    public static class NoOpInvocationHandler implements InvocationHandler, 
Serializable {
+
+        private static final long serialVersionUID = 1L;
+
+        @Override
+        public Object invoke(Object proxy, Method method, Object[] args) {
+            return null;
+        }
+    }
+
+
+    /*
+     * A dynamic proxy is deserialized via {@link 
CustomObjectInputStream#resolveProxyClass(String[])} rather than
+     * {@link 
CustomObjectInputStream#resolveClass(java.io.ObjectStreamClass)}. This test 
confirms that the configured
+     * {@code allowedClassNamePattern} is also applied to the interfaces 
implemented by a proxy class. If it is not, the
+     * class name filter can be bypassed simply by wrapping a disallowed 
interface in a dynamic proxy.
+     */
+    @Test
+    public void testResolveProxyClassAppliesAllowedClassNamePattern() throws 
Exception {
+
+        // A serializable dynamic proxy implementing java.lang.Runnable.
+        Object proxy = 
Proxy.newProxyInstance(TestCustomObjectInputStream.class.getClassLoader(),
+                new Class<?>[] { Runnable.class }, new 
NoOpInvocationHandler());
+
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
+            oos.writeObject(proxy);
+        }
+
+        // Permit everything that deserializing the proxy resolves via 
resolveClass() - the InvocationHandler in this
+        // package and the java.lang.reflect.Proxy superclass - but NOT the 
proxied java.lang.Runnable interface. That
+        // way the only class name that can trigger rejection is the 
interface, which is resolved via
+        // resolveProxyClass().
+        Pattern allowed = 
Pattern.compile("org\\.apache\\.catalina\\.util\\..*|java\\.lang\\.reflect\\.Proxy");
+
+        try (CustomObjectInputStream cois = new CustomObjectInputStream(
+                new ByteArrayInputStream(baos.toByteArray()), 
TestCustomObjectInputStream.class.getClassLoader(),
+                LogFactory.getLog(TestCustomObjectInputStream.class), allowed, 
false)) {
+
+            Assert.assertThrows("Deserialization of a proxy implementing a 
disallowed interface must be rejected",
+                    InvalidClassException.class, cois::readObject);
+        }
+    }
+
+
+    /*
+     * The counterpart to {@link 
#testResolveProxyClassAppliesAllowedClassNamePattern()}: when the proxied 
interface
+     * <em>does</em> match the configured {@code allowedClassNamePattern}, 
deserialization of the proxy must succeed.
+     */
+    @Test
+    public void testResolveProxyClassAllowsPermittedInterface() throws 
Exception {
+
+        // A serializable dynamic proxy implementing java.lang.Runnable.
+        Object proxy = 
Proxy.newProxyInstance(TestCustomObjectInputStream.class.getClassLoader(),
+                new Class<?>[] { Runnable.class }, new 
NoOpInvocationHandler());
+
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
+            oos.writeObject(proxy);
+        }
+
+        // Permit everything the proxy resolves, including the proxied 
java.lang.Runnable interface.
+        Pattern allowed = Pattern.compile(
+                
"org\\.apache\\.catalina\\.util\\..*|java\\.lang\\.reflect\\.Proxy|java\\.lang\\.Runnable");
+
+        try (CustomObjectInputStream cois = new CustomObjectInputStream(
+                new ByteArrayInputStream(baos.toByteArray()), 
TestCustomObjectInputStream.class.getClassLoader(),
+                LogFactory.getLog(TestCustomObjectInputStream.class), allowed, 
false)) {
+
+            Object result = cois.readObject();
+            Assert.assertTrue("Deserialized object should be a dynamic proxy", 
Proxy.isProxyClass(result.getClass()));
+            Assert.assertTrue("Deserialized proxy should implement the 
permitted interface", result instanceof Runnable);
+        }
+    }
+}
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index b726f6331f..7b74d2b46f 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -239,6 +239,10 @@
         Remove unnecessary calls to <code>String.intern()</code> in the parsing
         of configuration files. (markt)
       </scode>
+      <fix>
+        Extend <code>sessionAttributeValueClassNameFilter</code> to include
+        filtering of dynamic proxy interface classes. (markt)
+      </fix>
     </changelog>
   </subsection>
   <subsection name="Coyote">


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to