This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch camel-4.18.x
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/camel-4.18.x by this push:
new bb60b1d2f26a CAMEL-25585: camel-chunk - render message data containing
Chunk tags literally (#27873)
bb60b1d2f26a is described below
commit bb60b1d2f26a63ee144237f8087c0e993eaeb94d
Author: Andrea Cosentino <[email protected]>
AuthorDate: Thu Oct 15 14:02:44 2026 +0200
CAMEL-25585: camel-chunk - render message data containing Chunk tags
literally (#27873)
Co-Authored-By: Claude Opus 4.8 <[email protected]>
---
.../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 +
8 files changed, 232 insertions(+), 2 deletions(-)
diff --git a/components/camel-chunk/src/main/docs/chunk-component.adoc
b/components/camel-chunk/src/main/docs/chunk-component.adoc
index 3b6f44051a6b..109815e2caa0 100644
--- a/components/camel-chunk/src/main/docs/chunk-component.adoc
+++ b/components/camel-chunk/src/main/docs/chunk-component.adoc
@@ -141,4 +141,31 @@ Regards Camel Riders Bookstore
+== 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");
+----
+
include::spring-boot:partial$starter.adoc[]
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..625167a9f37e
--- /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.junit5.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