This is an automated email from the ASF dual-hosted git repository.
Croway 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 b6f6b4708543 CAMEL-24423: camel-tika - filter parsed document metadata
before mapping it to headers (#25819)
b6f6b4708543 is described below
commit b6f6b4708543677a8b6032304c66cbe7f687a2fb
Author: Andrea Cosentino <[email protected]>
AuthorDate: Fri Aug 28 12:08:02 2026 +0200
CAMEL-24423: camel-tika - filter parsed document metadata before mapping it
to headers (#25819)
TikaProducer.convertMetadataToHeaders() copied every metadata name produced
by the
parse straight onto the Camel message. Those names come out of the document
itself,
so a document could ask for any header name at all, including names in the
Camel-internal namespace - an HTML <meta name="CamelFileName"
content="../../x"/>
reached the message as CamelFileName and would then be picked up by a later
file:
producer.
Filter the names the same way a consumer filters names supplied by an
external
sender: a DefaultHeaderFilterStrategy with lowerCase matching and
inFilterStartsWith
of Camel, camel and org.apache.camel. A filtered name is skipped and logged
at DEBUG.
Metadata outside that namespace is mapped exactly as before.
Filtering rather than prefixing all parsed metadata keeps the change small
enough to
backport; prefixing would rename every header the component produces today.
Signed-off-by: Andrea Cosentino <[email protected]>
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
components/camel-tika/pom.xml | 5 ++
.../apache/camel/component/tika/TikaProducer.java | 25 ++++++--
.../tika/TikaMetadataHeaderFilterTest.java | 74 ++++++++++++++++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc | 17 +++++
4 files changed, 117 insertions(+), 4 deletions(-)
diff --git a/components/camel-tika/pom.xml b/components/camel-tika/pom.xml
index 33257b665291..a9409bf1c66e 100644
--- a/components/camel-tika/pom.xml
+++ b/components/camel-tika/pom.xml
@@ -86,6 +86,11 @@
<version>${hamcrest-version}</version>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.assertj</groupId>
+ <artifactId>assertj-core</artifactId>
+ <scope>test</scope>
+ </dependency>
</dependencies>
</project>
diff --git
a/components/camel-tika/src/main/java/org/apache/camel/component/tika/TikaProducer.java
b/components/camel-tika/src/main/java/org/apache/camel/component/tika/TikaProducer.java
index 9328dc47d344..6d8f2c2625c2 100644
---
a/components/camel-tika/src/main/java/org/apache/camel/component/tika/TikaProducer.java
+++
b/components/camel-tika/src/main/java/org/apache/camel/component/tika/TikaProducer.java
@@ -35,6 +35,8 @@ import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.apache.camel.Exchange;
+import org.apache.camel.spi.HeaderFilterStrategy;
+import org.apache.camel.support.DefaultHeaderFilterStrategy;
import org.apache.camel.support.DefaultProducer;
import org.apache.tika.config.TikaConfig;
import org.apache.tika.detect.Detector;
@@ -53,6 +55,8 @@ public class TikaProducer extends DefaultProducer {
private static final Logger LOG =
LoggerFactory.getLogger(TikaProducer.class);
+ private static final HeaderFilterStrategy HEADER_FILTER_STRATEGY =
createHeaderFilterStrategy();
+
private final TikaConfiguration tikaConfiguration;
private final Parser parser;
@@ -127,11 +131,15 @@ public class TikaProducer extends DefaultProducer {
if (metadata != null) {
for (String metaname : metadata.names()) {
String[] values = metadata.getValues(metaname);
- if (values.length == 1) {
- exchange.getIn().setHeader(metaname, values[0]);
- } else {
- exchange.getIn().setHeader(metaname, values);
+ Object value = values.length == 1 ? values[0] : values;
+ // The names come out of the parsed document, so they are
chosen by whoever produced it.
+ // Filter them the same way a consumer filters names supplied
by an external sender, so a
+ // document cannot declare a metadata name that lands in the
Camel-internal namespace.
+ if
(HEADER_FILTER_STRATEGY.applyFilterToExternalHeaders(metaname, value,
exchange)) {
+ LOG.debug("Skipping parsed metadata {} as the name is in
the Camel-internal namespace", metaname);
+ continue;
}
+ exchange.getIn().setHeader(metaname, value);
}
}
}
@@ -178,4 +186,13 @@ public class TikaProducer extends DefaultProducer {
return handler;
}
+
+ private static HeaderFilterStrategy createHeaderFilterStrategy() {
+ DefaultHeaderFilterStrategy strategy = new
DefaultHeaderFilterStrategy();
+ // Match case-insensitively, and cover the fully qualified form as
well as the Camel prefix
+ strategy.setLowerCase(true);
+ strategy.setInFilterStartsWith("Camel", "camel", "org.apache.camel.");
+ return strategy;
+ }
+
}
diff --git
a/components/camel-tika/src/test/java/org/apache/camel/component/tika/TikaMetadataHeaderFilterTest.java
b/components/camel-tika/src/test/java/org/apache/camel/component/tika/TikaMetadataHeaderFilterTest.java
new file mode 100644
index 000000000000..0a8d1e0c8108
--- /dev/null
+++
b/components/camel-tika/src/test/java/org/apache/camel/component/tika/TikaMetadataHeaderFilterTest.java
@@ -0,0 +1,74 @@
+/*
+ * 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.tika;
+
+import java.nio.charset.StandardCharsets;
+
+import org.apache.camel.EndpointInject;
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The metadata names handed to {@code convertMetadataToHeaders} come out of
the parsed document, so they are chosen by
+ * whoever produced it. An HTML {@code <meta name="..">} is the most direct
way to demonstrate that: the name attribute
+ * reaches Tika's metadata verbatim, so a document can ask for any header name
at all.
+ */
+class TikaMetadataHeaderFilterTest extends CamelTestSupport {
+
+ @EndpointInject("mock:result")
+ protected MockEndpoint resultEndpoint;
+
+ @Test
+ void documentMetadataCannotSetCamelInternalHeaders() throws Exception {
+ String html = "<html><head>"
+ + "<meta name=\"CamelFileName\"
content=\"../../pwned\"/>"
+ + "<meta name=\"camelfilename\"
content=\"../../pwned\"/>"
+ + "<meta name=\"CAMELHttpUri\"
content=\"http://other.example/x\"/>"
+ + "<meta name=\"org.apache.camel.internal\"
content=\"nope\"/>"
+ + "<meta name=\"author\" content=\"kept\"/>"
+ + "<title>t</title></head><body>hi</body></html>";
+
+ resultEndpoint.setExpectedMessageCount(1);
+ template.sendBody("direct:start",
html.getBytes(StandardCharsets.UTF_8));
+ resultEndpoint.assertIsSatisfied();
+
+ Exchange exchange = resultEndpoint.getExchanges().get(0);
+ assertThat(exchange.getIn().getHeader(Exchange.FILE_NAME)).isNull();
+ assertThat(exchange.getIn().getHeader("camelfilename")).isNull();
+ assertThat(exchange.getIn().getHeader("CAMELHttpUri")).isNull();
+
assertThat(exchange.getIn().getHeader("org.apache.camel.internal")).isNull();
+
+ // metadata outside the internal namespace is still mapped, so the
filter has not simply dropped everything
+ assertThat(exchange.getIn().getHeader("author")).isEqualTo("kept");
+ assertThat(exchange.getIn().getHeader("dc:title")).isEqualTo("t");
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:start").to("tika:parse").to("mock:result");
+ }
+ };
+ }
+}
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index afb004c9e82b..029f23952ec0 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -702,3 +702,20 @@ inherit values set on `defaultInstance`, and
`defaultInstance` itself remains un
Routes that compared unmarshalled bodies by identity, or that mutated one body
expecting the change to
be visible on another, must be updated.
+
+=== camel-tika
+
+The `tika:parse` producer copies the metadata of the parsed document onto the
Camel message. Those
+names come out of the document itself, so a document could ask for any header
name at all, including
+names in the Camel-internal namespace — an HTML `<meta name="CamelFileName"
content="..."/>`, for
+example, reached the message as `CamelFileName` and would then be picked up by
a later `file:`
+producer.
+
+Parsed metadata names are now filtered the same way a consumer filters names
supplied by an external
+sender: a name that starts with `Camel`, `camel` or `org.apache.camel.`
(matched case-insensitively)
+is skipped and logged at `DEBUG` instead of being set as a header. Metadata
outside that namespace is
+mapped exactly as before.
+
+Routes that deliberately read a `Camel`-prefixed header produced by the Tika
parse must set it
+themselves after the `tika:parse` step, for example with a `setHeader` reading
the corresponding
+non-prefixed metadata name.