Skip to content

CAMEL-24423/24442/24449/24453/24455/24475: untrusted input handling fixes, backport to camel-4.18.x - #25942

Merged
Croway merged 6 commits into
apache:camel-4.18.xfrom
oscerd:backport/glasswing-input-handling-4.18.x
Aug 31, 2026
Merged

CAMEL-24423/24442/24449/24453/24455/24475: untrusted input handling fixes, backport to camel-4.18.x#25942
Croway merged 6 commits into
apache:camel-4.18.xfrom
oscerd:backport/glasswing-input-handling-4.18.x

Conversation

@oscerd

@oscerd oscerd commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Backport to camel-4.18.x of six fixes already reviewed and merged on main. Each is a case where data arriving from outside the route reached somewhere it should not have.

The same set is going to camel-4.22.x in #25940.

Cherry-picked in the order they merged on main. Two mechanical adaptations for this branch, each folded into its own commit:

  • camel-thrift/pom.xml: the assertj test dependency is added alongside this branch's junit-toolbox entry rather than in its place.
  • ThriftUnmarshalIsolationTest and TikaMetadataHeaderFilterTest import camel-test-junit5; main is on junit6.

The upgrade-guide entries are not included: the guides for every line live on main.

Worth calling out for a patch release:

  • documentType=InputSource in camel-xpath now goes through the same hardened parser as the default document type, so a DOCTYPE in the payload is refused rather than resolved. The same DocumentBuilderFactory system properties that relaxed the default type still relax this one.
  • camel-tika no longer maps every parsed metadata key to a header. A route reading a metadata-derived header that the filter now blocks will see it missing.

Built and tested per module on this branch (core, camel-lra, camel-kafka, camel-platform-http, camel-thrift, camel-tika, camel-xpath), including the new and touched tests.

Claude Code on behalf of oscerd

oscerd and others added 6 commits August 31, 2026 10:23
…h, not by prefix (apache#25833)

isHttpProxy() tested path.startsWith(PROXY_PATH), so any endpoint whose path merely began
with "proxy" - proxyStats, proxy-health, proxying - was treated as the documented
platform-http:proxy endpoint. That is not only a naming curiosity: getPath() returns "/"
for such an endpoint, making it a catch-all, and VertxPlatformHttpConsumer.handleProxy()
sets Exchange.HTTP_HOST from the request's own Host header so a bridging producer
forwards there. A route author naming an endpoint proxyStats got a catch-all whose
forward target came from the caller.

Compare for equality. The check is deliberately strict rather than tolerating a leading
slash: platform-http:/proxy did not select proxy mode before and still does not, so
tightening this can never turn an endpoint into a proxy that was not already one. The
test asserts that, so the check is not loosened later by mistake.

Every platform-http:proxy usage in the tree - the component docs, PlatformHttpProxyTest,
VertxPlatformHttpProxyTest, VertxPlatformHttpsProxyTest - already uses the exact path.

Signed-off-by: Andrea Cosentino <ancosen@gmail.com>

(cherry picked from commit 7abf13b)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…insensitively when suppressing the echo (apache#25831)

enhanceHeaderFilterStrategyToSkipHttpRequestHeaders() keeps common request headers -
Authorization, Cookie, Proxy-Authorization and the rest of COMMON_HTTP_REQUEST_HEADERS -
from being echoed back on the response. The lookup was Set.contains(headerName) against
a canonically capitalised Set.of(...), while exchange headers keep the casing of the
inbound request: VertxPlatformHttpConsumer populates them from the Vert.x MultiMap as
received.

HTTP/2 requires field names to be lower case, so on an HTTP/2 request the names are
authorization, cookie and so on, none of which matched. The suppression therefore never
fired for HTTP/2 traffic, nor for any client that varied the casing, and
VertxPlatformHttpSupport.copyMessageHeadersToResponse wrote the headers to the response.

Hold the set in a TreeSet ordered by String.CASE_INSENSITIVE_ORDER so the comparison no
longer depends on how the client spelled the name.

Signed-off-by: Andrea Cosentino <ancosen@gmail.com>

(cherry picked from commit 126c79b)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ed defaultInstance (apache#25823)

* CAMEL-24442: camel-thrift - unmarshal into a copy instead of the shared defaultInstance

ThriftDataFormat.unmarshal() deserialized into the defaultInstance field and returned
that same object. The data format is shared by every exchange on the route, and
Thrift's TBase.read() assigns only the fields present in the incoming bytes without
clearing the object first, so:

- a message that omitted an optional field kept the value left there by the previous
  message - deterministic, no concurrency needed;
- concurrent unmarshals interleaved field writes into the one object;
- every in-flight body was literally the same reference.

Deserialize into defaultInstance.deepCopy() and return that. ProtobufDataFormat
already builds a new instance per unmarshal. As a side effect defaultInstance is left
untouched and now works as the template its name promises: values preset on it are
visible on every message, where before the first message overwrote them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Andrea Cosentino <ancosen@gmail.com>

* CAMEL-24442: Clear copied Thrift instance before unmarshal

---------

Signed-off-by: Andrea Cosentino <ancosen@gmail.com>

(cherry picked from commit c52d3cf)

The assertj test dependency is added alongside this branch's junit-toolbox entry
rather than in its place.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ing it to headers (apache#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 <ancosen@gmail.com>

(cherry picked from commit b6f6b47)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e hardened XML parser (apache#25683)

XPathBuilder handed an InputSource straight to XPathExpression, which builds a
DocumentBuilder of its own with the JDK defaults - so documentType=InputSource
(and SAXSource) accepted a DOCTYPE declaration and resolved external entities,
while the default documentType of Document did not.

All four evaluation sites now convert through the type converter, reusing the
same hardened DocumentBuilderFactory the default document type already goes
through. This adds no document parse: evaluate(InputSource) already built a
full DOM internally.

(cherry picked from commit 1ead256)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
… service that uses it

SagaProcessor.getCurrentSagaCoordinator() fell back to the unprefixed
Long-Running-Action message header whenever the exchange's internal saga
state was missing, so it could pick up a coordinator id from an
unrelated caller and join that exchange to the wrong saga under AUTO
completion. That fallback was added for LRA protocol interoperability
(CAMEL-23469), but applied unconditionally to every saga service,
including the default InMemorySagaService, which has no external
coordinator to interoperate with.

Adds CamelSagaService.isLongRunningActionHeaderSupported(), defaulting
to false, and only consults the header when the configured service
opts in. LRASagaService overrides it to true, preserving the existing
interoperability. A custom CamelSagaService joining sagas via the
header must now override this method. KafkaSagaIT is updated to
advertise support since its saga id only survives a Kafka round-trip
through the header. Includes an upgrade-guide entry for 4.23.

Closes apache#25828

(cherry picked from commit dfde003)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Croway
Croway merged commit af1efbc into apache:camel-4.18.x Aug 31, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants