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