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]