diff --git a/components/camel-http-common/src/main/java/org/apache/camel/http/common/HttpHelper.java b/components/camel-http-common/src/main/java/org/apache/camel/http/common/HttpHelper.java index a95af16da5f09..93195b91d95b4 100644 --- a/components/camel-http-common/src/main/java/org/apache/camel/http/common/HttpHelper.java +++ b/components/camel-http-common/src/main/java/org/apache/camel/http/common/HttpHelper.java @@ -142,8 +142,7 @@ public static Object deserializeJavaObjectFromStream(InputStream is, CamelContex } Object answer; - ObjectInputStream ois = new CamelObjectInputStream(is, context); - ois.setObjectInputFilter(DeserializationFilterHelper.resolveDeserializationFilter(deserializationFilter)); + ObjectInputStream ois = new CamelObjectInputStream(is, context, deserializationFilter); try { answer = ois.readObject(); } finally { diff --git a/core/camel-support/src/main/java/org/apache/camel/support/CamelObjectInputStream.java b/core/camel-support/src/main/java/org/apache/camel/support/CamelObjectInputStream.java index 927160ed3a8af..c870414919011 100644 --- a/core/camel-support/src/main/java/org/apache/camel/support/CamelObjectInputStream.java +++ b/core/camel-support/src/main/java/org/apache/camel/support/CamelObjectInputStream.java @@ -18,21 +18,48 @@ import java.io.IOException; import java.io.InputStream; +import java.io.ObjectInputFilter; import java.io.ObjectInputStream; import java.io.ObjectStreamClass; import org.apache.camel.CamelContext; +/** + * An {@link ObjectInputStream} that resolves classes against the Camel application classloader and installs a JEP-290 + * {@link ObjectInputFilter} while reading, as a defense-in-depth measure against unsafe deserialization. + * + *
+ * As this is the shared stream used by Camel deserialization consumers, the filter is applied by default so that every + * caller inherits it. When no explicit pattern is supplied the JVM-wide {@code jdk.serialFilter} is honoured if set, + * otherwise {@link DeserializationFilterHelper#DEFAULT_DESERIALIZATION_FILTER} is applied. + */ public class CamelObjectInputStream extends ObjectInputStream { private final ClassLoader classLoader; public CamelObjectInputStream(InputStream in, CamelContext context) throws IOException { + this(in, context, null); + } + + /** + * Creates a {@link CamelObjectInputStream} that applies a JEP-290 {@link ObjectInputFilter} while reading. + * + * @param in the input stream to read from + * @param context the camel context used to resolve the application classloader; may be {@code null} + * @param deserializationFilter an {@link ObjectInputFilter} pattern (same syntax as {@code jdk.serialFilter}) to + * apply; when {@code null} or blank the JVM-wide {@code jdk.serialFilter} is used if + * present, otherwise + * {@link DeserializationFilterHelper#DEFAULT_DESERIALIZATION_FILTER} is applied + * @throws IOException if an I/O error occurs while reading the stream header + * @since 4.22 + */ + public CamelObjectInputStream(InputStream in, CamelContext context, String deserializationFilter) throws IOException { super(in); if (context != null) { this.classLoader = context.getApplicationContextClassLoader(); } else { this.classLoader = null; } + setObjectInputFilter(DeserializationFilterHelper.resolveDeserializationFilter(deserializationFilter)); } @Override diff --git a/core/camel-support/src/test/java/org/apache/camel/support/CamelObjectInputStreamTest.java b/core/camel-support/src/test/java/org/apache/camel/support/CamelObjectInputStreamTest.java new file mode 100644 index 0000000000000..bcbf8095dcd02 --- /dev/null +++ b/core/camel-support/src/test/java/org/apache/camel/support/CamelObjectInputStreamTest.java @@ -0,0 +1,78 @@ +/* + * 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.camel.support; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InvalidClassException; +import java.io.ObjectOutputStream; +import java.net.InetSocketAddress; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class CamelObjectInputStreamTest { + + @Test + void defaultFilterAllowsStandardTypes() throws Exception { + assertEquals("hello camel", deserialize(serialize("hello camel"))); + } + + @Test + void defaultFilterRejectsClassOutsideAllowList() throws Exception { + byte[] data = serialize(InetSocketAddress.createUnresolved("example.com", 8080)); + InvalidClassException ex = assertThrows(InvalidClassException.class, () -> deserialize(data)); + assertTrue(ex.getMessage().contains("REJECTED"), ex.getMessage()); + } + + @Test + void blankFilterFallsBackToDefault() throws Exception { + byte[] data = serialize(InetSocketAddress.createUnresolved("example.com", 8080)); + InvalidClassException ex = assertThrows(InvalidClassException.class, () -> deserialize(data, " ")); + assertTrue(ex.getMessage().contains("REJECTED"), ex.getMessage()); + } + + @Test + void explicitFilterCanAllowOtherwiseDeniedClass() throws Exception { + InetSocketAddress address = InetSocketAddress.createUnresolved("example.com", 8080); + assertEquals(address, deserialize(serialize(address), "java.**;!*")); + } + + private static byte[] serialize(Object value) throws IOException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + try (ObjectOutputStream oos = new ObjectOutputStream(bos)) { + oos.writeObject(value); + } + return bos.toByteArray(); + } + + private static Object deserialize(byte[] data) throws IOException, ClassNotFoundException { + try (CamelObjectInputStream ois = new CamelObjectInputStream(new ByteArrayInputStream(data), null)) { + return ois.readObject(); + } + } + + private static Object deserialize(byte[] data, String filter) throws IOException, ClassNotFoundException { + try (CamelObjectInputStream ois = new CamelObjectInputStream(new ByteArrayInputStream(data), null, filter)) { + return ois.readObject(); + } + } +} diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc index a6b8fb0663265..2f0bac5411270 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc @@ -55,6 +55,21 @@ recipient; aligning it with `toD` / `enrich` is deferred to a follow-up. If you need a placeholder resolved by `toD` / `enrich`, keep it in the route's endpoint URI rather than in the message. +=== camel-support - CamelObjectInputStream applies a deserialization filter by default + +`CamelObjectInputStream` (the shared stream used by Camel's Java-object deserialization paths, such +as the HTTP components) now installs a JEP-290 `java.io.ObjectInputFilter` while reading, as a +defense-in-depth measure against unsafe deserialization. When no explicit filter pattern is +supplied, the JVM-wide `jdk.serialFilter` is honoured if set, otherwise Camel's default allow-list +(`DeserializationFilterHelper.DEFAULT_DESERIALIZATION_FILTER`) is applied, which permits standard +Java and Apache Camel types, denies `java.net.**`, and enforces graph-shape limits. + +The built-in HTTP deserialization path already applied this filter, so most users are unaffected. +Code that constructs `CamelObjectInputStream` directly and deserializes types outside the default +allow-list must pass an explicit filter pattern to the new +`CamelObjectInputStream(InputStream, CamelContext, String)` constructor (or configure +`jdk.serialFilter`) to permit them. + === camel-jbang The Camel JBang CLI (Camel CLI) and TUI have been promoted from _Preview_ to _Stable_ support level.