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

davsclaus 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 6fdf569ef50c CAMEL-25585: camel-chunk - render message data literally 
instead of re-parsing it as Chunk template code (#27721)
6fdf569ef50c is described below

commit 6fdf569ef50c56605e06a31f9240deacb9d59d8d
Author: Andrea Cosentino <[email protected]>
AuthorDate: Wed Oct 14 21:20:06 2026 +0200

    CAMEL-25585: camel-chunk - render message data literally instead of 
re-parsing it as Chunk template code (#27721)
    
    Co-Authored-By: Claude Opus 4.8 <[email protected]>
---
 .../apache/camel/catalog/docs/chunk-component.adoc |  27 +++++
 .../camel-chunk/src/main/docs/chunk-component.adoc |  27 +++++
 .../camel/component/chunk/ChunkEndpoint.java       |  77 ++++++++++++-
 .../component/chunk/ChunkValueExpansionTest.java   | 125 +++++++++++++++++++++
 .../src/test/resources/themes/bodykey.chtml        |   1 +
 .../src/test/resources/themes/headervalue.chtml    |   1 +
 .../src/test/resources/themes/loopmaps.chtml       |   1 +
 .../src/test/resources/themes/slicebody.chtml      |   1 +
 .../src/test/resources/themes/varvalue.chtml       |   1 +
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  24 ++++
 10 files changed, 283 insertions(+), 2 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/chunk-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/chunk-component.adoc
index eb725f0f38a5..09378c38b316 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/chunk-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/chunk-component.adoc
@@ -261,3 +261,30 @@ Thanks for the order of {$headers.item}.
 Regards Camel Riders Bookstore
 {$body}
 ----------------------------------------------
+
+== Security
+
+The Chunk engine renders the template provided by the route author. Message 
data (the body, headers and
+variables) that is printed with a direct tag such as `{$body}` or 
`{$headers.name}` is rendered literally, so
+template tags that appear inside that data are printed verbatim and not 
evaluated.
+
+Chunk does, however, re-parse any string it produces while rendering. The 
output of a filter (`{$body|trim}`),
+a string property of a bean body (`{$body.name}`, `{$exception.message}`) and 
string items bound by a
+`{% loop %}` are parsed as template code. Do not pass untrusted message data 
through Chunk filters, bean
+properties or loops, and do not enable `allowTemplateFromHeader` for messages 
from untrusted senders.
+
+Chunk has no escape for tags that appear inside data, so if untrusted data 
must go through one of those paths,
+remove the `{` and `}` characters from it in the route before the `chunk` 
endpoint, for example:
+
+[source,java]
+----
+from("direct:start")
+    // strip Chunk tag delimiters from the untrusted body before templating
+    .process(e -> {
+        String body = e.getMessage().getBody(String.class);
+        if (body != null) {
+            e.getMessage().setBody(body.replace("{", "").replace("}", ""));
+        }
+    })
+    .to("chunk:mytemplate");
+----
diff --git a/components/camel-chunk/src/main/docs/chunk-component.adoc 
b/components/camel-chunk/src/main/docs/chunk-component.adoc
index eb725f0f38a5..09378c38b316 100644
--- a/components/camel-chunk/src/main/docs/chunk-component.adoc
+++ b/components/camel-chunk/src/main/docs/chunk-component.adoc
@@ -261,3 +261,30 @@ Thanks for the order of {$headers.item}.
 Regards Camel Riders Bookstore
 {$body}
 ----------------------------------------------
+
+== Security
+
+The Chunk engine renders the template provided by the route author. Message 
data (the body, headers and
+variables) that is printed with a direct tag such as `{$body}` or 
`{$headers.name}` is rendered literally, so
+template tags that appear inside that data are printed verbatim and not 
evaluated.
+
+Chunk does, however, re-parse any string it produces while rendering. The 
output of a filter (`{$body|trim}`),
+a string property of a bean body (`{$body.name}`, `{$exception.message}`) and 
string items bound by a
+`{% loop %}` are parsed as template code. Do not pass untrusted message data 
through Chunk filters, bean
+properties or loops, and do not enable `allowTemplateFromHeader` for messages 
from untrusted senders.
+
+Chunk has no escape for tags that appear inside data, so if untrusted data 
must go through one of those paths,
+remove the `{` and `}` characters from it in the route before the `chunk` 
endpoint, for example:
+
+[source,java]
+----
+from("direct:start")
+    // strip Chunk tag delimiters from the untrusted body before templating
+    .process(e -> {
+        String body = e.getMessage().getBody(String.class);
+        if (body != null) {
+            e.getMessage().setBody(body.replace("{", "").replace("}", ""));
+        }
+    })
+    .to("chunk:mytemplate");
+----
diff --git 
a/components/camel-chunk/src/main/java/org/apache/camel/component/chunk/ChunkEndpoint.java
 
b/components/camel-chunk/src/main/java/org/apache/camel/component/chunk/ChunkEndpoint.java
index e6ed0bc1c2d1..701c2303acc4 100644
--- 
a/components/camel-chunk/src/main/java/org/apache/camel/component/chunk/ChunkEndpoint.java
+++ 
b/components/camel-chunk/src/main/java/org/apache/camel/component/chunk/ChunkEndpoint.java
@@ -22,15 +22,21 @@ import java.io.Reader;
 import java.io.StringReader;
 import java.io.StringWriter;
 import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.List;
 import java.util.Map;
 
 import com.x5.template.Chunk;
+import com.x5.template.Snippet;
 import com.x5.template.Theme;
 import org.apache.camel.Category;
 import org.apache.camel.Component;
 import org.apache.camel.Exchange;
 import org.apache.camel.ExchangePattern;
 import org.apache.camel.component.ResourceEndpoint;
+import org.apache.camel.spi.HeadersMapFactory;
 import org.apache.camel.spi.UriEndpoint;
 import org.apache.camel.spi.UriParam;
 import org.apache.camel.support.ExchangeHelper;
@@ -117,15 +123,20 @@ public class ChunkEndpoint extends ResourceEndpoint {
                 exchange.getIn().removeHeader(CHUNK_TEMPLATE);
             }
 
-            // Execute Chunk
+            // Execute Chunk. Bind the message data as literal values so that 
the Chunk engine renders
+            // the body, headers and variables as-is and does not re-parse 
(and expand) any template tags
+            // that may be present inside the data itself. Only the route 
author's template is parsed as
+            // template code.
             Map<String, Object> variableMap = 
ExchangeHelper.createVariableMap(exchange, isAllowContextMapAll());
+            HeadersMapFactory factory = 
getCamelContext().getCamelContextExtension().getHeadersMapFactory();
+            variableMap.replaceAll((key, value) -> toLiteralValue(value, 
factory));
             StringWriter writer = new StringWriter();
             newChunk.putAll(variableMap);
             newChunk.render(writer);
             writer.flush();
 
             // Fill out message
-            ExchangeHelper.setInOutBodyPatternAware(exchange, 
newChunk.toString());
+            ExchangeHelper.setInOutBodyPatternAware(exchange, 
writer.toString());
         } else {
             exchange.getIn().removeHeader(ChunkConstants.CHUNK_RESOURCE_URI);
             ChunkEndpoint newEndpoint
@@ -134,6 +145,68 @@ public class ChunkEndpoint extends ResourceEndpoint {
         }
     }
 
+    /**
+     * Neutralizes message data so the Chunk engine does not parse and expand 
template tags present inside the data
+     * itself when it is printed with a direct {@code {$tag}}. A String (or 
other {@link CharSequence}) value is wrapped
+     * as a literal Chunk snippet only when it actually contains Chunk tags 
(as reported by Chunk's own parser via
+     * {@link Snippet#isSimple()}); tag-free values are left as-is so Chunk's 
string filters (for example {@code slice},
+     * {@code get}, {@code join}) keep their existing behaviour. Nested {@link 
Map}, {@link Collection} and array values
+     * (message headers, variables, multi-value headers, unmarshalled JSON, 
...) are copied and processed recursively,
+     * preserving a case-insensitive headers map. Other value types are left 
unchanged.
+     * <p/>
+     * This does not cover every case: Chunk re-parses any string it produces 
while rendering, so the output of a filter
+     * ({@code {$body|trim}}), a string property of a bean body ({@code 
{$body.name}}, {@code {$exception.message}}) and
+     * string items produced inside a {@code {% loop %}} are still parsed as 
template code. Untrusted data must not be
+     * passed through Chunk filters, bean properties or loops.
+     */
+    @SuppressWarnings("unchecked")
+    private static Object toLiteralValue(Object value, HeadersMapFactory 
factory) {
+        if (value instanceof String) {
+            return toLiteralString((String) value);
+        }
+        if (value instanceof CharSequence) {
+            // StringBuilder/StringBuffer and other CharSequences are 
otherwise stringified and re-parsed by Chunk
+            return toLiteralString(value.toString());
+        }
+        if (value instanceof Map) {
+            Map<?, ?> map = (Map<?, ?>) value;
+            Map<String, Object> copy = factory.isInstanceOf((Map<String, 
Object>) value)
+                    ? factory.newMap() : new LinkedHashMap<>();
+            for (Map.Entry<?, ?> entry : map.entrySet()) {
+                copy.put(String.valueOf(entry.getKey()), 
toLiteralValue(entry.getValue(), factory));
+            }
+            return copy;
+        }
+        if (value instanceof Collection) {
+            Collection<?> collection = (Collection<?>) value;
+            List<Object> copy = new ArrayList<>(collection.size());
+            for (Object item : collection) {
+                copy.add(toLiteralValue(item, factory));
+            }
+            return copy;
+        }
+        if (value instanceof Object[]) {
+            Object[] array = (Object[]) value;
+            List<Object> copy = new ArrayList<>(array.length);
+            for (Object item : array) {
+                copy.add(toLiteralValue(item, factory));
+            }
+            return copy;
+        }
+        return value;
+    }
+
+    /**
+     * Wraps the given string as a literal Chunk snippet when it contains 
Chunk tags, otherwise returns it unchanged so
+     * tag-free data keeps its normal (filter-aware) behaviour.
+     */
+    private static Object toLiteralString(String str) {
+        if (!Snippet.getSnippet(str).isSimple()) {
+            return Snippet.makeLiteralSnippet(str);
+        }
+        return str;
+    }
+
     /**
      * Create a Chunk template
      *
diff --git 
a/components/camel-chunk/src/test/java/org/apache/camel/component/chunk/ChunkValueExpansionTest.java
 
b/components/camel-chunk/src/test/java/org/apache/camel/component/chunk/ChunkValueExpansionTest.java
new file mode 100644
index 000000000000..ce5d592257a6
--- /dev/null
+++ 
b/components/camel-chunk/src/test/java/org/apache/camel/component/chunk/ChunkValueExpansionTest.java
@@ -0,0 +1,125 @@
+/*
+ * 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.chunk;
+
+import java.util.List;
+import java.util.Map;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Verifies that message data (body, headers, variables) that contains Chunk 
tags is rendered as literal text and is not
+ * re-parsed and expanded as Chunk template code, while tag-free data keeps 
its normal behaviour (including Chunk's
+ * string filters). Only the route author's template should be parsed as 
template code.
+ */
+class ChunkValueExpansionTest extends CamelTestSupport {
+
+    @Test
+    void bodyValueIsPrintedLiterally() {
+        // The body itself contains Chunk markup; it must be printed as-is, 
not expanded.
+        Exchange out = template.request("direct:body", e -> {
+            e.getIn().setBody("{$headers.name}");
+            e.getIn().setHeader("name", "World");
+        });
+        assertEquals("This is the body {$headers.name}\n", 
out.getMessage().getBody(String.class));
+    }
+
+    @Test
+    void headerValueIsPrintedLiterally() {
+        // A header value containing Chunk markup must be printed as-is, not 
expanded.
+        Exchange out = template.request("direct:header", e -> {
+            e.getIn().setHeader("value", "{$headers.name}");
+            e.getIn().setHeader("name", "World");
+        });
+        assertEquals("Header is {$headers.name}\n", 
out.getMessage().getBody(String.class));
+    }
+
+    @Test
+    void variableValueIsPrintedLiterally() {
+        // A variable value containing Chunk markup must be printed as-is, not 
expanded.
+        Exchange out = template.request("direct:var", e -> {
+            e.setVariable("v", "{$headers.name}");
+            e.getIn().setHeader("name", "World");
+        });
+        assertEquals("Var is {$headers.name}\n", 
out.getMessage().getBody(String.class));
+    }
+
+    @Test
+    void mapBodyValueIsPrintedLiterally() {
+        // A nested value inside a Map body containing Chunk markup must be 
printed as-is, not expanded.
+        Exchange out = template.request("direct:bodykey", e -> {
+            e.getIn().setBody(Map.of("key", "{$headers.name}"));
+            e.getIn().setHeader("name", "World");
+        });
+        assertEquals("Body key is {$headers.name}\n", 
out.getMessage().getBody(String.class));
+    }
+
+    @Test
+    void stringBuilderBodyIsPrintedLiterally() {
+        // A non-String CharSequence body containing Chunk markup must be 
printed as-is, not expanded.
+        Exchange out = template.request("direct:body", e -> {
+            e.getIn().setBody(new StringBuilder("{$headers.name}"));
+            e.getIn().setHeader("name", "World");
+        });
+        assertEquals("This is the body {$headers.name}\n", 
out.getMessage().getBody(String.class));
+    }
+
+    @Test
+    void listOfMapsValueIsPrintedLiterally() {
+        // A nested value inside a List of Maps (e.g. unmarshalled JSON) 
containing Chunk markup must be literal.
+        Exchange out = template.request("direct:loopmaps", e -> {
+            e.getIn().setBody(List.of(Map.of("k", "{$headers.name}")));
+            e.getIn().setHeader("name", "World");
+        });
+        assertEquals("[{$headers.name}]", 
out.getMessage().getBody(String.class));
+    }
+
+    @Test
+    void stringFiltersStillWorkOnPlainData() {
+        // Tag-free data is left as a String, so Chunk's string filters behave 
as before.
+        Exchange out = template.request("direct:slice", e -> 
e.getIn().setBody("Hello World"));
+        assertEquals("Hello\n", out.getMessage().getBody(String.class));
+    }
+
+    @Test
+    void normalRenderingStillWorks() {
+        // Tags in the template are still resolved against the data as usual.
+        Exchange out = template.request("direct:hello", e -> 
e.getIn().setHeader("name", "Andrew"));
+        assertEquals("Earth to Andrew. Come in, Andrew.\n", 
out.getMessage().getBody(String.class));
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:body").to("chunk://nested");
+                from("direct:header").to("chunk://headervalue");
+                from("direct:var").to("chunk://varvalue");
+                from("direct:bodykey").to("chunk://bodykey");
+                from("direct:loopmaps").to("chunk://loopmaps");
+                from("direct:slice").to("chunk://slicebody");
+                from("direct:hello").to("chunk://hello");
+            }
+        };
+    }
+}
diff --git a/components/camel-chunk/src/test/resources/themes/bodykey.chtml 
b/components/camel-chunk/src/test/resources/themes/bodykey.chtml
new file mode 100644
index 000000000000..d24c032d1805
--- /dev/null
+++ b/components/camel-chunk/src/test/resources/themes/bodykey.chtml
@@ -0,0 +1 @@
+Body key is {$body.key}
\ No newline at end of file
diff --git a/components/camel-chunk/src/test/resources/themes/headervalue.chtml 
b/components/camel-chunk/src/test/resources/themes/headervalue.chtml
new file mode 100644
index 000000000000..96d4cae9a5d2
--- /dev/null
+++ b/components/camel-chunk/src/test/resources/themes/headervalue.chtml
@@ -0,0 +1 @@
+Header is {$headers.value}
\ No newline at end of file
diff --git a/components/camel-chunk/src/test/resources/themes/loopmaps.chtml 
b/components/camel-chunk/src/test/resources/themes/loopmaps.chtml
new file mode 100644
index 000000000000..e0f88d63346c
--- /dev/null
+++ b/components/camel-chunk/src/test/resources/themes/loopmaps.chtml
@@ -0,0 +1 @@
+{% loop in $body as x %}[{$x.k}]{% endloop %}
\ No newline at end of file
diff --git a/components/camel-chunk/src/test/resources/themes/slicebody.chtml 
b/components/camel-chunk/src/test/resources/themes/slicebody.chtml
new file mode 100644
index 000000000000..be3c4fcff398
--- /dev/null
+++ b/components/camel-chunk/src/test/resources/themes/slicebody.chtml
@@ -0,0 +1 @@
+{$body|slice(0:5)}
\ No newline at end of file
diff --git a/components/camel-chunk/src/test/resources/themes/varvalue.chtml 
b/components/camel-chunk/src/test/resources/themes/varvalue.chtml
new file mode 100644
index 000000000000..ac3f14c24eec
--- /dev/null
+++ b/components/camel-chunk/src/test/resources/themes/varvalue.chtml
@@ -0,0 +1 @@
+Var is {$variables.v}
\ No newline at end of file
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index 92cb5dd34a98..8401ad2ae1d2 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -523,6 +523,30 @@ Previously it returned only the first chunk of the event 
log (at most 500 events
 any sign. A wide `after`/`before` window now returns every event in one list: 
that is one request per 500 events plus
 one that finds the end of the log, and more memory. Narrow the window when the 
event log is large.
 
+=== camel-chunk - message data printed with a direct tag is rendered literally
+
+Previously the `chunk` component bound the message body, headers and variables 
into the Chunk engine so that
+Chunk re-parsed those values as template source: a value that itself contained 
Chunk tags (such as a body of
+`{$headers.name}`) had those tags expanded instead of being printed verbatim.
+
+Message data bound to the engine is now neutralised so that a direct `{$tag}` 
printing the body, a header, a
+variable, or an element of a nested map, list or array renders the value 
literally instead of evaluating the
+tags inside it. Tag-free data is unchanged and Chunk's string filters (for 
example `slice`, `get`, `join`)
+keep their existing behaviour. Routes that relied on message data being 
evaluated as Chunk template code must
+move that markup into the template itself.
+
+As part of this, a non-`List` `Collection` value (such as a `Set`) is now 
bound as a `List`, so a direct
+`{$body}` prints Chunk's list placeholder instead of the collection's 
`toString()`, and a filter such as
+`|join` operates on its elements.
+
+This does not cover every path: Chunk re-parses any string it produces while 
rendering, so the output of a
+filter (`{$body|trim}`), a string property of a bean body (`{$body.name}`, 
`{$exception.message}`) and string
+items bound by a `{% loop %}` are still parsed as template code. Keep 
untrusted data to direct `{$tag}`
+output; do not pass untrusted values through Chunk filters, bean properties or 
loops. If untrusted data must
+go through one of those paths, remove the `{` and `}` characters from it in 
the route before the `chunk`
+endpoint (Chunk has no escape for tags inside data), for example with a 
processor that rewrites the body or
+header.
+
 === camel-telegram - a failed send fails the exchange once
 
 When Telegram answers a message with an error status (for example `400` for an 
unknown chat), or with a body that

Reply via email to