This is an automated email from the ASF dual-hosted git repository.
oscerd 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 48485d408f19 CAMEL-24894: camel-docling - make output path handling
consistent with input path and custom-argument handling (#26737)
48485d408f19 is described below
commit 48485d408f190c46ef408ba4ba75e45b4e8ffb04
Author: Andrea Cosentino <[email protected]>
AuthorDate: Fri Sep 25 11:01:26 2026 +0200
CAMEL-24894: camel-docling - make output path handling consistent with
input path and custom-argument handling (#26737)
* CAMEL-24894: camel-docling - make output path handling consistent with
input path and custom-argument handling
The CamelDoclingOutputFilePath header was passed straight to the docling
CLI --output flag, while input file paths are normalized and confined to
inputBaseDirectory when set, and custom-argument values are normalized via
validatePathSafety.
Normalize the output header value and add an optional outputBaseDirectory
option mirroring inputBaseDirectory, so the output directory can be confined
the same way. Both remain unset by default, preserving the previous
behavior. Adds DoclingOutputPathValidationTest and documents the option in
the component docs and the 4.23 upgrade guide.
Co-authored-by: Claude Opus 4.8 <[email protected]>
Signed-off-by: Andrea Cosentino <[email protected]>
* Regen
* CAMEL-24894: camel-docling - clarify that output path normalization is
unconditional in docs
Review feedback: the upgrade guide and component doc implied the
CamelDoclingOutputFilePath value is unchanged when outputBaseDirectory is
unset. Normalization actually applies unconditionally; only the
base-directory containment is gated behind the option. Reword both to
state that.
Co-authored-by: Claude Opus 4.8 <[email protected]>
Signed-off-by: Andrea Cosentino <[email protected]>
* CAMEL-24894: camel-docling - strengthen output path test and document the
lexical containment boundary
Address review feedback:
- assert the value that actually reaches --output via a command-capture
test (out/./sub/../x -> out/x), instead of only checking the failure
message
- use a genuinely relative "../outside" in the traversal test; @TempDir
hands
out absolute paths, so the old value exercised the absolute-path branch
- document that the outputBaseDirectory containment is lexical and does not
resolve symbolic links (matching inputBaseDirectory) in the option
description, the helper javadoc and the component docs
Co-authored-by: Claude Opus 4.8 <[email protected]>
Signed-off-by: Andrea Cosentino <[email protected]>
* CAMEL-24894: camel-docling - drop reflection from the output path test
helper
Address review nits (non-blocking):
- make buildDoclingCommand package-private (visible for testing) and call it
directly, so a signature change becomes a compile error rather than a
runtime NoSuchMethodException on the normalization test
- comment the bare baseDir() calls that exist for their directory-creation
side effect
Co-authored-by: Claude Opus 4.8 <[email protected]>
Signed-off-by: Andrea Cosentino <[email protected]>
---------
Signed-off-by: Andrea Cosentino <[email protected]>
Co-authored-by: Claude Opus 4.8 <[email protected]>
Co-authored-by: Guillaume Nodet <[email protected]>
---
.../apache/camel/catalog/components/docling.json | 6 +-
.../camel/catalog/docs/docling-component.adoc | 13 +-
.../docling/DoclingComponentConfigurer.java | 6 +
.../docling/DoclingConfigurationConfigurer.java | 6 +
.../docling/DoclingEndpointConfigurer.java | 6 +
.../docling/DoclingEndpointUriFactory.java | 3 +-
.../apache/camel/component/docling/docling.json | 6 +-
.../src/main/docs/docling-component.adoc | 13 +-
.../component/docling/DoclingConfiguration.java | 16 ++
.../camel/component/docling/DoclingProducer.java | 39 ++++-
.../docling/DoclingOutputPathValidationTest.java | 180 +++++++++++++++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc | 7 +
.../dsl/DoclingComponentBuilderFactory.java | 21 +++
.../dsl/DoclingEndpointBuilderFactory.java | 19 +++
14 files changed, 329 insertions(+), 12 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/docling.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/docling.json
index 38f6f0002620..c44d5d62ca9c 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/docling.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/docling.json
@@ -79,7 +79,8 @@
"authenticationToken": { "index": 52, "kind": "property", "displayName":
"Authentication Token", "group": "security", "label": "security", "required":
false, "type": "string", "javaType": "java.lang.String", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": true, "security":
"secret", "configurationClass":
"org.apache.camel.component.docling.DoclingConfiguration",
"configurationField": "configuration", "description": "Authentication token for
docling-serve API [...]
"inputBaseDirectory": { "index": 53, "kind": "property", "displayName":
"Input Base Directory", "group": "security", "label": "security", "required":
false, "type": "string", "javaType": "java.lang.String", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false,
"configurationClass":
"org.apache.camel.component.docling.DoclingConfiguration",
"configurationField": "configuration", "description": "When set, every local
input file path must resolve inside this d [...]
"maxFileSize": { "index": 54, "kind": "property", "displayName": "Max File
Size", "group": "security", "label": "security", "required": false, "type":
"integer", "javaType": "long", "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "defaultValue": 52428800,
"configurationClass":
"org.apache.camel.component.docling.DoclingConfiguration",
"configurationField": "configuration", "description": "Maximum file size in
bytes for processing" },
- "oauthProfile": { "index": 55, "kind": "property", "displayName": "Oauth
Profile", "group": "security", "label": "security", "required": false, "type":
"string", "javaType": "java.lang.String", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false,
"configurationClass":
"org.apache.camel.component.docling.DoclingConfiguration",
"configurationField": "configuration", "description": "OAuth profile name for
obtaining an access token via the OAuth 2.0 Client Cre [...]
+ "oauthProfile": { "index": 55, "kind": "property", "displayName": "Oauth
Profile", "group": "security", "label": "security", "required": false, "type":
"string", "javaType": "java.lang.String", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false,
"configurationClass":
"org.apache.camel.component.docling.DoclingConfiguration",
"configurationField": "configuration", "description": "OAuth profile name for
obtaining an access token via the OAuth 2.0 Client Cre [...]
+ "outputBaseDirectory": { "index": 56, "kind": "property", "displayName":
"Output Base Directory", "group": "security", "label": "security", "required":
false, "type": "string", "javaType": "java.lang.String", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false,
"configurationClass":
"org.apache.camel.component.docling.DoclingConfiguration",
"configurationField": "configuration", "description": "When set, the output
directory passed to the docling CLI must [...]
},
"headers": {
"CamelDoclingOperation": { "index": 0, "kind": "header", "displayName":
"", "group": "producer", "label": "", "required": false, "javaType":
"DoclingOperations", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The operation to perform",
"constantName": "org.apache.camel.component.docling.DoclingHeaders#OPERATION" },
@@ -170,6 +171,7 @@
"authenticationToken": { "index": 51, "kind": "parameter", "displayName":
"Authentication Token", "group": "security", "label": "security", "required":
false, "type": "string", "javaType": "java.lang.String", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": true, "security":
"secret", "configurationClass":
"org.apache.camel.component.docling.DoclingConfiguration",
"configurationField": "configuration", "description": "Authentication token for
docling-serve AP [...]
"inputBaseDirectory": { "index": 52, "kind": "parameter", "displayName":
"Input Base Directory", "group": "security", "label": "security", "required":
false, "type": "string", "javaType": "java.lang.String", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false,
"configurationClass":
"org.apache.camel.component.docling.DoclingConfiguration",
"configurationField": "configuration", "description": "When set, every local
input file path must resolve inside this [...]
"maxFileSize": { "index": 53, "kind": "parameter", "displayName": "Max
File Size", "group": "security", "label": "security", "required": false,
"type": "integer", "javaType": "long", "deprecated": false, "deprecationNote":
"", "autowired": false, "secret": false, "defaultValue": 52428800,
"configurationClass":
"org.apache.camel.component.docling.DoclingConfiguration",
"configurationField": "configuration", "description": "Maximum file size in
bytes for processing" },
- "oauthProfile": { "index": 54, "kind": "parameter", "displayName": "Oauth
Profile", "group": "security", "label": "security", "required": false, "type":
"string", "javaType": "java.lang.String", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false,
"configurationClass":
"org.apache.camel.component.docling.DoclingConfiguration",
"configurationField": "configuration", "description": "OAuth profile name for
obtaining an access token via the OAuth 2.0 Client Cr [...]
+ "oauthProfile": { "index": 54, "kind": "parameter", "displayName": "Oauth
Profile", "group": "security", "label": "security", "required": false, "type":
"string", "javaType": "java.lang.String", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false,
"configurationClass":
"org.apache.camel.component.docling.DoclingConfiguration",
"configurationField": "configuration", "description": "OAuth profile name for
obtaining an access token via the OAuth 2.0 Client Cr [...]
+ "outputBaseDirectory": { "index": 55, "kind": "parameter", "displayName":
"Output Base Directory", "group": "security", "label": "security", "required":
false, "type": "string", "javaType": "java.lang.String", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false,
"configurationClass":
"org.apache.camel.component.docling.DoclingConfiguration",
"configurationField": "configuration", "description": "When set, the output
directory passed to the docling CLI must [...]
}
}
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/docling-component.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/docling-component.adoc
index b6b6d355a855..e23a16a20eb1 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/docling-component.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/docling-component.adoc
@@ -149,6 +149,11 @@ directory-or-file `String` body accepted by the batch
operations.
| _(none)_
| When set, every local input path must resolve inside this directory once
normalized. Applies to the
`CamelDoclingInputFilePath` header, to file path bodies, and to the paths used
by the batch operations.
+
+| `outputBaseDirectory`
+| _(none)_
+| When set, the output directory passed to the docling CLI must resolve inside
this directory once normalized.
+Applies to the `CamelDoclingOutputFilePath` header.
|===
With both options left at their defaults, a body that is neither a URL nor a
path is written to a temporary
@@ -159,6 +164,11 @@ keeps working without `allowFilePathSource`. It is still
subject to `inputBaseDi
Typed bodies - `File`, `byte[]`, `InputStream`, and the explicit path
collections used by the batch
operations - are unambiguous and are likewise unaffected.
+The `CamelDoclingOutputFilePath` header, which selects the CLI `--output`
directory, is normalized lexically
+and, when `outputBaseDirectory` is set, confined to that directory. The
normalization applies unconditionally;
+with `outputBaseDirectory` unset, a header value without traversal segments is
otherwise used as given. The
+containment is lexical and does not resolve symbolic links (as with
`inputBaseDirectory`).
+
[source,java]
----
// the body is the document itself - no opt-in needed
@@ -482,7 +492,8 @@ Only the following flags are permitted:
|===
The `--output` (`-o`) flag is **not permitted** because the output directory
is managed by the producer.
-Use the `CamelDoclingOutputFilePath` header or endpoint configuration instead.
+Use the `CamelDoclingOutputFilePath` header or endpoint configuration instead.
That header is normalized and,
+when `outputBaseDirectory` is set, confined to that directory (see the
security options above).
Additionally, the following are rejected:
diff --git
a/components/camel-ai/camel-docling/src/generated/java/org/apache/camel/component/docling/DoclingComponentConfigurer.java
b/components/camel-ai/camel-docling/src/generated/java/org/apache/camel/component/docling/DoclingComponentConfigurer.java
index db93f1ea2817..294d0b4d5e9d 100644
---
a/components/camel-ai/camel-docling/src/generated/java/org/apache/camel/component/docling/DoclingComponentConfigurer.java
+++
b/components/camel-ai/camel-docling/src/generated/java/org/apache/camel/component/docling/DoclingComponentConfigurer.java
@@ -120,6 +120,8 @@ public class DoclingComponentConfigurer extends
PropertyConfigurerSupport implem
case "ocrlanguage":
case "ocrLanguage":
getOrCreateConfiguration(target).setOcrLanguage(property(camelContext,
java.lang.String.class, value)); return true;
case "operation":
getOrCreateConfiguration(target).setOperation(property(camelContext,
org.apache.camel.component.docling.DoclingOperations.class, value)); return
true;
+ case "outputbasedirectory":
+ case "outputBaseDirectory":
getOrCreateConfiguration(target).setOutputBaseDirectory(property(camelContext,
java.lang.String.class, value)); return true;
case "outputformat":
case "outputFormat":
getOrCreateConfiguration(target).setOutputFormat(property(camelContext,
java.lang.String.class, value)); return true;
case "pdfbackend":
@@ -236,6 +238,8 @@ public class DoclingComponentConfigurer extends
PropertyConfigurerSupport implem
case "ocrlanguage":
case "ocrLanguage": return java.lang.String.class;
case "operation": return
org.apache.camel.component.docling.DoclingOperations.class;
+ case "outputbasedirectory":
+ case "outputBaseDirectory": return java.lang.String.class;
case "outputformat":
case "outputFormat": return java.lang.String.class;
case "pdfbackend":
@@ -353,6 +357,8 @@ public class DoclingComponentConfigurer extends
PropertyConfigurerSupport implem
case "ocrlanguage":
case "ocrLanguage": return
getOrCreateConfiguration(target).getOcrLanguage();
case "operation": return
getOrCreateConfiguration(target).getOperation();
+ case "outputbasedirectory":
+ case "outputBaseDirectory": return
getOrCreateConfiguration(target).getOutputBaseDirectory();
case "outputformat":
case "outputFormat": return
getOrCreateConfiguration(target).getOutputFormat();
case "pdfbackend":
diff --git
a/components/camel-ai/camel-docling/src/generated/java/org/apache/camel/component/docling/DoclingConfigurationConfigurer.java
b/components/camel-ai/camel-docling/src/generated/java/org/apache/camel/component/docling/DoclingConfigurationConfigurer.java
index acba9c6e6930..eef584583d45 100644
---
a/components/camel-ai/camel-docling/src/generated/java/org/apache/camel/component/docling/DoclingConfigurationConfigurer.java
+++
b/components/camel-ai/camel-docling/src/generated/java/org/apache/camel/component/docling/DoclingConfigurationConfigurer.java
@@ -108,6 +108,8 @@ public class DoclingConfigurationConfigurer extends
org.apache.camel.support.com
case "ocrlanguage":
case "ocrLanguage": target.setOcrLanguage(property(camelContext,
java.lang.String.class, value)); return true;
case "operation": target.setOperation(property(camelContext,
org.apache.camel.component.docling.DoclingOperations.class, value)); return
true;
+ case "outputbasedirectory":
+ case "outputBaseDirectory":
target.setOutputBaseDirectory(property(camelContext, java.lang.String.class,
value)); return true;
case "outputformat":
case "outputFormat": target.setOutputFormat(property(camelContext,
java.lang.String.class, value)); return true;
case "pdfbackend":
@@ -219,6 +221,8 @@ public class DoclingConfigurationConfigurer extends
org.apache.camel.support.com
case "ocrlanguage":
case "ocrLanguage": return java.lang.String.class;
case "operation": return
org.apache.camel.component.docling.DoclingOperations.class;
+ case "outputbasedirectory":
+ case "outputBaseDirectory": return java.lang.String.class;
case "outputformat":
case "outputFormat": return java.lang.String.class;
case "pdfbackend":
@@ -331,6 +335,8 @@ public class DoclingConfigurationConfigurer extends
org.apache.camel.support.com
case "ocrlanguage":
case "ocrLanguage": return target.getOcrLanguage();
case "operation": return target.getOperation();
+ case "outputbasedirectory":
+ case "outputBaseDirectory": return target.getOutputBaseDirectory();
case "outputformat":
case "outputFormat": return target.getOutputFormat();
case "pdfbackend":
diff --git
a/components/camel-ai/camel-docling/src/generated/java/org/apache/camel/component/docling/DoclingEndpointConfigurer.java
b/components/camel-ai/camel-docling/src/generated/java/org/apache/camel/component/docling/DoclingEndpointConfigurer.java
index 483440f5e27a..9e686a34e913 100644
---
a/components/camel-ai/camel-docling/src/generated/java/org/apache/camel/component/docling/DoclingEndpointConfigurer.java
+++
b/components/camel-ai/camel-docling/src/generated/java/org/apache/camel/component/docling/DoclingEndpointConfigurer.java
@@ -110,6 +110,8 @@ public class DoclingEndpointConfigurer extends
PropertyConfigurerSupport impleme
case "ocrlanguage":
case "ocrLanguage":
target.getConfiguration().setOcrLanguage(property(camelContext,
java.lang.String.class, value)); return true;
case "operation":
target.getConfiguration().setOperation(property(camelContext,
org.apache.camel.component.docling.DoclingOperations.class, value)); return
true;
+ case "outputbasedirectory":
+ case "outputBaseDirectory":
target.getConfiguration().setOutputBaseDirectory(property(camelContext,
java.lang.String.class, value)); return true;
case "outputformat":
case "outputFormat":
target.getConfiguration().setOutputFormat(property(camelContext,
java.lang.String.class, value)); return true;
case "pdfbackend":
@@ -223,6 +225,8 @@ public class DoclingEndpointConfigurer extends
PropertyConfigurerSupport impleme
case "ocrlanguage":
case "ocrLanguage": return java.lang.String.class;
case "operation": return
org.apache.camel.component.docling.DoclingOperations.class;
+ case "outputbasedirectory":
+ case "outputBaseDirectory": return java.lang.String.class;
case "outputformat":
case "outputFormat": return java.lang.String.class;
case "pdfbackend":
@@ -337,6 +341,8 @@ public class DoclingEndpointConfigurer extends
PropertyConfigurerSupport impleme
case "ocrlanguage":
case "ocrLanguage": return target.getConfiguration().getOcrLanguage();
case "operation": return target.getConfiguration().getOperation();
+ case "outputbasedirectory":
+ case "outputBaseDirectory": return
target.getConfiguration().getOutputBaseDirectory();
case "outputformat":
case "outputFormat": return
target.getConfiguration().getOutputFormat();
case "pdfbackend":
diff --git
a/components/camel-ai/camel-docling/src/generated/java/org/apache/camel/component/docling/DoclingEndpointUriFactory.java
b/components/camel-ai/camel-docling/src/generated/java/org/apache/camel/component/docling/DoclingEndpointUriFactory.java
index e0462bf46ed4..1974f8bffba2 100644
---
a/components/camel-ai/camel-docling/src/generated/java/org/apache/camel/component/docling/DoclingEndpointUriFactory.java
+++
b/components/camel-ai/camel-docling/src/generated/java/org/apache/camel/component/docling/DoclingEndpointUriFactory.java
@@ -24,7 +24,7 @@ public class DoclingEndpointUriFactory extends
org.apache.camel.support.componen
private static final Set<String> ENDPOINT_IDENTITY_PROPERTY_NAMES;
private static final Map<String, String> MULTI_VALUE_PREFIXES;
static {
- Set<String> props = new HashSet<>(55);
+ Set<String> props = new HashSet<>(56);
props.add("abortOnError");
props.add("allowFilePathSource");
props.add("allowUrlSource");
@@ -70,6 +70,7 @@ public class DoclingEndpointUriFactory extends
org.apache.camel.support.componen
props.add("ocrLanguage");
props.add("operation");
props.add("operationId");
+ props.add("outputBaseDirectory");
props.add("outputFormat");
props.add("pdfBackend");
props.add("pipeline");
diff --git
a/components/camel-ai/camel-docling/src/generated/resources/META-INF/org/apache/camel/component/docling/docling.json
b/components/camel-ai/camel-docling/src/generated/resources/META-INF/org/apache/camel/component/docling/docling.json
index 38f6f0002620..c44d5d62ca9c 100644
---
a/components/camel-ai/camel-docling/src/generated/resources/META-INF/org/apache/camel/component/docling/docling.json
+++
b/components/camel-ai/camel-docling/src/generated/resources/META-INF/org/apache/camel/component/docling/docling.json
@@ -79,7 +79,8 @@
"authenticationToken": { "index": 52, "kind": "property", "displayName":
"Authentication Token", "group": "security", "label": "security", "required":
false, "type": "string", "javaType": "java.lang.String", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": true, "security":
"secret", "configurationClass":
"org.apache.camel.component.docling.DoclingConfiguration",
"configurationField": "configuration", "description": "Authentication token for
docling-serve API [...]
"inputBaseDirectory": { "index": 53, "kind": "property", "displayName":
"Input Base Directory", "group": "security", "label": "security", "required":
false, "type": "string", "javaType": "java.lang.String", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false,
"configurationClass":
"org.apache.camel.component.docling.DoclingConfiguration",
"configurationField": "configuration", "description": "When set, every local
input file path must resolve inside this d [...]
"maxFileSize": { "index": 54, "kind": "property", "displayName": "Max File
Size", "group": "security", "label": "security", "required": false, "type":
"integer", "javaType": "long", "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "defaultValue": 52428800,
"configurationClass":
"org.apache.camel.component.docling.DoclingConfiguration",
"configurationField": "configuration", "description": "Maximum file size in
bytes for processing" },
- "oauthProfile": { "index": 55, "kind": "property", "displayName": "Oauth
Profile", "group": "security", "label": "security", "required": false, "type":
"string", "javaType": "java.lang.String", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false,
"configurationClass":
"org.apache.camel.component.docling.DoclingConfiguration",
"configurationField": "configuration", "description": "OAuth profile name for
obtaining an access token via the OAuth 2.0 Client Cre [...]
+ "oauthProfile": { "index": 55, "kind": "property", "displayName": "Oauth
Profile", "group": "security", "label": "security", "required": false, "type":
"string", "javaType": "java.lang.String", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false,
"configurationClass":
"org.apache.camel.component.docling.DoclingConfiguration",
"configurationField": "configuration", "description": "OAuth profile name for
obtaining an access token via the OAuth 2.0 Client Cre [...]
+ "outputBaseDirectory": { "index": 56, "kind": "property", "displayName":
"Output Base Directory", "group": "security", "label": "security", "required":
false, "type": "string", "javaType": "java.lang.String", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false,
"configurationClass":
"org.apache.camel.component.docling.DoclingConfiguration",
"configurationField": "configuration", "description": "When set, the output
directory passed to the docling CLI must [...]
},
"headers": {
"CamelDoclingOperation": { "index": 0, "kind": "header", "displayName":
"", "group": "producer", "label": "", "required": false, "javaType":
"DoclingOperations", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The operation to perform",
"constantName": "org.apache.camel.component.docling.DoclingHeaders#OPERATION" },
@@ -170,6 +171,7 @@
"authenticationToken": { "index": 51, "kind": "parameter", "displayName":
"Authentication Token", "group": "security", "label": "security", "required":
false, "type": "string", "javaType": "java.lang.String", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": true, "security":
"secret", "configurationClass":
"org.apache.camel.component.docling.DoclingConfiguration",
"configurationField": "configuration", "description": "Authentication token for
docling-serve AP [...]
"inputBaseDirectory": { "index": 52, "kind": "parameter", "displayName":
"Input Base Directory", "group": "security", "label": "security", "required":
false, "type": "string", "javaType": "java.lang.String", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false,
"configurationClass":
"org.apache.camel.component.docling.DoclingConfiguration",
"configurationField": "configuration", "description": "When set, every local
input file path must resolve inside this [...]
"maxFileSize": { "index": 53, "kind": "parameter", "displayName": "Max
File Size", "group": "security", "label": "security", "required": false,
"type": "integer", "javaType": "long", "deprecated": false, "deprecationNote":
"", "autowired": false, "secret": false, "defaultValue": 52428800,
"configurationClass":
"org.apache.camel.component.docling.DoclingConfiguration",
"configurationField": "configuration", "description": "Maximum file size in
bytes for processing" },
- "oauthProfile": { "index": 54, "kind": "parameter", "displayName": "Oauth
Profile", "group": "security", "label": "security", "required": false, "type":
"string", "javaType": "java.lang.String", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false,
"configurationClass":
"org.apache.camel.component.docling.DoclingConfiguration",
"configurationField": "configuration", "description": "OAuth profile name for
obtaining an access token via the OAuth 2.0 Client Cr [...]
+ "oauthProfile": { "index": 54, "kind": "parameter", "displayName": "Oauth
Profile", "group": "security", "label": "security", "required": false, "type":
"string", "javaType": "java.lang.String", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false,
"configurationClass":
"org.apache.camel.component.docling.DoclingConfiguration",
"configurationField": "configuration", "description": "OAuth profile name for
obtaining an access token via the OAuth 2.0 Client Cr [...]
+ "outputBaseDirectory": { "index": 55, "kind": "parameter", "displayName":
"Output Base Directory", "group": "security", "label": "security", "required":
false, "type": "string", "javaType": "java.lang.String", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false,
"configurationClass":
"org.apache.camel.component.docling.DoclingConfiguration",
"configurationField": "configuration", "description": "When set, the output
directory passed to the docling CLI must [...]
}
}
diff --git
a/components/camel-ai/camel-docling/src/main/docs/docling-component.adoc
b/components/camel-ai/camel-docling/src/main/docs/docling-component.adoc
index b6b6d355a855..e23a16a20eb1 100644
--- a/components/camel-ai/camel-docling/src/main/docs/docling-component.adoc
+++ b/components/camel-ai/camel-docling/src/main/docs/docling-component.adoc
@@ -149,6 +149,11 @@ directory-or-file `String` body accepted by the batch
operations.
| _(none)_
| When set, every local input path must resolve inside this directory once
normalized. Applies to the
`CamelDoclingInputFilePath` header, to file path bodies, and to the paths used
by the batch operations.
+
+| `outputBaseDirectory`
+| _(none)_
+| When set, the output directory passed to the docling CLI must resolve inside
this directory once normalized.
+Applies to the `CamelDoclingOutputFilePath` header.
|===
With both options left at their defaults, a body that is neither a URL nor a
path is written to a temporary
@@ -159,6 +164,11 @@ keeps working without `allowFilePathSource`. It is still
subject to `inputBaseDi
Typed bodies - `File`, `byte[]`, `InputStream`, and the explicit path
collections used by the batch
operations - are unambiguous and are likewise unaffected.
+The `CamelDoclingOutputFilePath` header, which selects the CLI `--output`
directory, is normalized lexically
+and, when `outputBaseDirectory` is set, confined to that directory. The
normalization applies unconditionally;
+with `outputBaseDirectory` unset, a header value without traversal segments is
otherwise used as given. The
+containment is lexical and does not resolve symbolic links (as with
`inputBaseDirectory`).
+
[source,java]
----
// the body is the document itself - no opt-in needed
@@ -482,7 +492,8 @@ Only the following flags are permitted:
|===
The `--output` (`-o`) flag is **not permitted** because the output directory
is managed by the producer.
-Use the `CamelDoclingOutputFilePath` header or endpoint configuration instead.
+Use the `CamelDoclingOutputFilePath` header or endpoint configuration instead.
That header is normalized and,
+when `outputBaseDirectory` is set, confined to that directory (see the
security options above).
Additionally, the following are rejected:
diff --git
a/components/camel-ai/camel-docling/src/main/java/org/apache/camel/component/docling/DoclingConfiguration.java
b/components/camel-ai/camel-docling/src/main/java/org/apache/camel/component/docling/DoclingConfiguration.java
index ed381e76e51b..4c9b6df1f799 100644
---
a/components/camel-ai/camel-docling/src/main/java/org/apache/camel/component/docling/DoclingConfiguration.java
+++
b/components/camel-ai/camel-docling/src/main/java/org/apache/camel/component/docling/DoclingConfiguration.java
@@ -91,6 +91,14 @@ public class DoclingConfiguration implements Cloneable {
+ " restriction is applied.")
private String inputBaseDirectory;
+ @UriParam(label = "security")
+ @Metadata(description = "When set, the output directory passed to the
docling CLI must resolve inside this"
+ + " directory once normalized. Applies to the
CamelDoclingOutputFilePath header. The"
+ + " check is lexical and does not resolve symbolic
links, matching inputBaseDirectory."
+ + " When empty, no directory restriction is
applied and the header value is only"
+ + " normalized.")
+ private String outputBaseDirectory;
+
@UriParam
@Metadata(description = "Include the content of the output file in the
exchange body and delete the output file",
defaultValue = "false")
@@ -362,6 +370,14 @@ public class DoclingConfiguration implements Cloneable {
this.inputBaseDirectory = inputBaseDirectory;
}
+ public String getOutputBaseDirectory() {
+ return outputBaseDirectory;
+ }
+
+ public void setOutputBaseDirectory(String outputBaseDirectory) {
+ this.outputBaseDirectory = outputBaseDirectory;
+ }
+
public boolean isContentInBody() {
return contentInBody;
}
diff --git
a/components/camel-ai/camel-docling/src/main/java/org/apache/camel/component/docling/DoclingProducer.java
b/components/camel-ai/camel-docling/src/main/java/org/apache/camel/component/docling/DoclingProducer.java
index 556f3ed5a1e5..e36ec9615d92 100644
---
a/components/camel-ai/camel-docling/src/main/java/org/apache/camel/component/docling/DoclingProducer.java
+++
b/components/camel-ai/camel-docling/src/main/java/org/apache/camel/component/docling/DoclingProducer.java
@@ -2121,7 +2121,9 @@ public class DoclingProducer extends DefaultProducer {
}
}
- private List<String> buildDoclingCommand(String inputPath, String
outputFormat, Exchange exchange, String outputDirectory) {
+ // package-private so DoclingOutputPathValidationTest can assert the built
command without reflection
+ List<String> buildDoclingCommand(String inputPath, String outputFormat,
Exchange exchange, String outputDirectory)
+ throws IOException {
List<String> command = new ArrayList<>();
command.add(configuration.getDoclingCommand());
@@ -2282,17 +2284,44 @@ public class DoclingProducer extends DefaultProducer {
}
}
- private void addOutputDirectoryArguments(List<String> command, Exchange
exchange, String outputDirectory) {
+ private void addOutputDirectoryArguments(List<String> command, Exchange
exchange, String outputDirectory)
+ throws IOException {
String outputPath =
exchange.getIn().getHeader(DoclingHeaders.OUTPUT_FILE_PATH, String.class);
+ command.add("--output");
if (outputPath != null) {
- command.add("--output");
- command.add(outputPath);
+ // the header is caller-provided, so it gets the same
normalization and optional base-directory
+ // containment as input paths do, instead of reaching the CLI
verbatim
+
command.add(resolveWithinOutputBaseDirectory(outputPath).toString());
} else {
- command.add("--output");
command.add(outputDirectory);
}
}
+ /**
+ * Normalizes the given output directory and, when {@code
outputBaseDirectory} is configured, verifies that it stays
+ * inside that directory. Mirrors {@link
#resolveWithinInputBaseDirectory(String)} so that the output directory
+ * carried by the {@link DoclingHeaders#OUTPUT_FILE_PATH} header receives
the same treatment as input paths. The
+ * containment check is lexical: symbolic links are not resolved, so - like
+ * {@link #resolveWithinInputBaseDirectory(String)} - a symlink inside the
base directory that points outside it is
+ * not detected.
+ */
+ private Path resolveWithinOutputBaseDirectory(String outputPath) throws
IOException {
+ String base = configuration.getOutputBaseDirectory();
+ if (base == null || base.isEmpty()) {
+ // no directory restriction: normalize lexically only, and leave
relative paths relative so that they keep
+ // resolving the way they did before - against the CLI working
directory, when one is set
+ return Paths.get(outputPath).normalize();
+ }
+ Path baseDir = Paths.get(base).toAbsolutePath().normalize();
+ // resolve relative paths against the base directory itself, so that
the path checked here is exactly the path
+ // used downstream regardless of the process working directory; an
absolute header value that escapes is rejected
+ Path path = baseDir.resolve(Paths.get(outputPath)).normalize();
+ if (!path.startsWith(baseDir)) {
+ throw new IOException("Output path resolves outside of
outputBaseDirectory (" + baseDir + "): " + outputPath);
+ }
+ return path;
+ }
+
private String mapToDoclingFormat(String outputFormat) {
switch (outputFormat.toLowerCase()) {
case "markdown":
diff --git
a/components/camel-ai/camel-docling/src/test/java/org/apache/camel/component/docling/DoclingOutputPathValidationTest.java
b/components/camel-ai/camel-docling/src/test/java/org/apache/camel/component/docling/DoclingOutputPathValidationTest.java
new file mode 100644
index 000000000000..4bf2ec47cc5b
--- /dev/null
+++
b/components/camel-ai/camel-docling/src/test/java/org/apache/camel/component/docling/DoclingOutputPathValidationTest.java
@@ -0,0 +1,180 @@
+/*
+ * 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.docling;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+
+import org.apache.camel.CamelExecutionException;
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * Tests that the {@code CamelDoclingOutputFilePath} header is normalized and,
when {@code outputBaseDirectory} is
+ * configured, confined to that directory before it reaches the docling CLI
{@code --output} flag, consistently with how
+ * input paths honour {@code inputBaseDirectory}.
+ */
+class DoclingOutputPathValidationTest extends CamelTestSupport {
+
+ private static final String CONTENT = "just some document text";
+
+ @TempDir
+ Path tempDir;
+
+ // ------------------------------------------------------- no
outputBaseDirectory
+
+ @Test
+ void outputPathWithoutBaseDirectoryIsAllowed() {
+ // no restriction is configured: the header is normalized only and
passes through, so the run fails later on the
+ // absent docling binary rather than on a containment check
+ assertThatThrownBy(() ->
template.requestBodyAndHeader("direct:default", CONTENT,
+ DoclingHeaders.OUTPUT_FILE_PATH,
tempDir.resolve("out").toString()))
+ .isInstanceOf(CamelExecutionException.class)
+ .cause()
+ .hasMessageNotContaining("outputBaseDirectory");
+ }
+
+ @Test
+ void outputPathIsNormalizedInTheBuiltCommand() throws Exception {
+ // with outputBaseDirectory unset (the default, and the branch most
routes hit) the header is still
+ // normalized lexically before it reaches --output: out/./sub/../x
collapses to out/x
+ List<String> command = buildDoclingCommandFor("out/./sub/../x");
+
+ int i = command.indexOf("--output");
+ assertThat(i).isGreaterThanOrEqualTo(0);
+ assertThat(command.get(i + 1))
+ .isEqualTo(Paths.get("out", "x").toString())
+ .doesNotContain("..");
+ }
+
+ // ------------------------------------------------------
outputBaseDirectory jail
+
+ @Test
+ void outputPathInsideOutputBaseDirectoryIsAccepted() throws Exception {
+ String inside = baseDir().resolve("out").toString();
+
+ // the docling binary is absent so execution still fails, but it must
not fail on the jail check
+ assertThatThrownBy(() ->
template.requestBodyAndHeader("direct:jailed", CONTENT,
+ DoclingHeaders.OUTPUT_FILE_PATH, inside))
+ .isInstanceOf(CamelExecutionException.class)
+ .cause()
+ .hasMessageNotContaining("outputBaseDirectory");
+ }
+
+ @Test
+ void outputPathOutsideOutputBaseDirectoryIsRejected() throws Exception {
+ baseDir(); // create the directory the jailed route points at via
outputBaseDirectory
+ String outside = tempDir.resolve("outside").toString();
+
+ assertThatThrownBy(() ->
template.requestBodyAndHeader("direct:jailed", CONTENT,
+ DoclingHeaders.OUTPUT_FILE_PATH, outside))
+ .isInstanceOf(CamelExecutionException.class)
+ .cause()
+ .isInstanceOf(IOException.class)
+ .hasMessageContaining("outputBaseDirectory");
+ }
+
+ @Test
+ void absoluteOutputPathOutsideOutputBaseDirectoryIsRejected() throws
Exception {
+ baseDir(); // create the directory the jailed route points at via
outputBaseDirectory
+
+ // an absolute header value ignores the base directory when resolved,
so it must be rejected as escaping
+ assertThatThrownBy(() ->
template.requestBodyAndHeader("direct:jailed", CONTENT,
+ DoclingHeaders.OUTPUT_FILE_PATH, "/var/www/html/uploads"))
+ .isInstanceOf(CamelExecutionException.class)
+ .cause()
+ .isInstanceOf(IOException.class)
+ .hasMessageContaining("outputBaseDirectory");
+ }
+
+ @Test
+ void traversalOutOfOutputBaseDirectoryIsRejected() throws Exception {
+ baseDir(); // create the directory the jailed route points at via
outputBaseDirectory
+
+ // a genuinely relative value, so the baseDir.resolve(..) +
normalize() branch is exercised; an absolute
+ // value would instead hit the same branch as
absoluteOutputPathOutsideOutputBaseDirectoryIsRejected
+ assertThatThrownBy(() ->
template.requestBodyAndHeader("direct:jailed", CONTENT,
+ DoclingHeaders.OUTPUT_FILE_PATH, "../outside"))
+ .isInstanceOf(CamelExecutionException.class)
+ .cause()
+ .isInstanceOf(IOException.class)
+ .hasMessageContaining("outputBaseDirectory");
+ }
+
+ @Test
+ void siblingDirectorySharingANamePrefixIsRejected() throws Exception {
+ // "<base>-evil" shares a string prefix with "<base>" but is not
inside it; a plain String.startsWith
+ // comparison would wrongly accept this
+ baseDir(); // create the directory the jailed route points at via
outputBaseDirectory
+ String sibling =
tempDir.resolve("base-evil").resolve("out").toString();
+
+ assertThatThrownBy(() ->
template.requestBodyAndHeader("direct:jailed", CONTENT,
+ DoclingHeaders.OUTPUT_FILE_PATH, sibling))
+ .isInstanceOf(CamelExecutionException.class)
+ .cause()
+ .isInstanceOf(IOException.class)
+ .hasMessageContaining("outputBaseDirectory");
+ }
+
+ // ------------------------------------------------------------------
configuration
+
+ @Test
+ void outputBaseDirectoryDefaultsToNull() {
+ assertThat(new
DoclingConfiguration().getOutputBaseDirectory()).isNull();
+ }
+
+ private Path baseDir() throws IOException {
+ return Files.createDirectories(tempDir.resolve("base"));
+ }
+
+ private List<String> buildDoclingCommandFor(String outputHeader) throws
Exception {
+ // outputBaseDirectory is unset on this endpoint, so
buildDoclingCommand exercises the no-base branch
+ DoclingEndpoint endpoint
+ =
context.getEndpoint("docling:convert?operation=CONVERT_TO_MARKDOWN",
DoclingEndpoint.class);
+ DoclingProducer producer = (DoclingProducer) endpoint.createProducer();
+ Exchange exchange = endpoint.createExchange();
+ exchange.getIn().setHeader(DoclingHeaders.OUTPUT_FILE_PATH,
outputHeader);
+
+ return producer.buildDoclingCommand("input.pdf", "markdown", exchange,
"/tmp/managed");
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() throws Exception {
+ String missingBinary =
tempDir.resolve("no-such-docling").toString();
+
+ from("direct:default")
+
.to("docling:convert?operation=CONVERT_TO_MARKDOWN&doclingCommand=" +
missingBinary);
+
+ from("direct:jailed")
+ .to("docling:convert?operation=CONVERT_TO_MARKDOWN"
+ + "&outputBaseDirectory=" + baseDir() +
"&doclingCommand=" + missingBinary);
+ }
+ };
+ }
+}
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 6b12d19acaab..27a65d70d6c5 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
@@ -918,6 +918,13 @@ It is unset by default, which keeps the previous behaviour
of accepting any path
Additionally, a local input path that does not exist is now reported as a
`File not found` `IOException`
before Docling is invoked. Previously the size check silently skipped a path
that resolved to nothing and
the failure surfaced later, from the Docling process or API call.
+
+The `CamelDoclingOutputFilePath` header, which selects the CLI output
directory, is now normalized lexically
+(redundant separators and `.`/`..` segments are resolved) before it is passed
to Docling; a header value
+without such segments is still used as given. A new `outputBaseDirectory`
option additionally confines the
+header the same way `inputBaseDirectory` confines input paths - when set, an
output path that resolves outside
+it, including an absolute path, is rejected with an `IOException`.
`outputBaseDirectory` is unset by default.
+
=== camel-azure-eventgrid
The `CamelAzureEventGridDataVersion` header
(`EventGridConstants.DATA_VERSION`) has been removed. The
diff --git
a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/DoclingComponentBuilderFactory.java
b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/DoclingComponentBuilderFactory.java
index a389a92afdc5..6ea5ab37571c 100644
---
a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/DoclingComponentBuilderFactory.java
+++
b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/DoclingComponentBuilderFactory.java
@@ -1009,6 +1009,26 @@ public interface DoclingComponentBuilderFactory {
doSetProperty("oauthProfile", oauthProfile);
return this;
}
+
+ /**
+ * When set, the output directory passed to the docling CLI must
resolve
+ * inside this directory once normalized. Applies to the
+ * CamelDoclingOutputFilePath header. The check is lexical and does not
+ * resolve symbolic links, matching inputBaseDirectory. When empty, no
+ * directory restriction is applied and the header value is only
+ * normalized.
+ *
+ * The option is a: <code>java.lang.String</code> type.
+ *
+ * Group: security
+ *
+ * @param outputBaseDirectory the value to set
+ * @return the dsl builder
+ */
+ default DoclingComponentBuilder outputBaseDirectory(java.lang.String
outputBaseDirectory) {
+ doSetProperty("outputBaseDirectory", outputBaseDirectory);
+ return this;
+ }
}
class DoclingComponentBuilderImpl
@@ -1086,6 +1106,7 @@ public interface DoclingComponentBuilderFactory {
case "inputBaseDirectory":
getOrCreateConfiguration((DoclingComponent)
component).setInputBaseDirectory((java.lang.String) value); return true;
case "maxFileSize": getOrCreateConfiguration((DoclingComponent)
component).setMaxFileSize((long) value); return true;
case "oauthProfile": getOrCreateConfiguration((DoclingComponent)
component).setOauthProfile((java.lang.String) value); return true;
+ case "outputBaseDirectory":
getOrCreateConfiguration((DoclingComponent)
component).setOutputBaseDirectory((java.lang.String) value); return true;
default: return false;
}
}
diff --git
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/DoclingEndpointBuilderFactory.java
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/DoclingEndpointBuilderFactory.java
index 0d73a7a8156b..15781f4ac764 100644
---
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/DoclingEndpointBuilderFactory.java
+++
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/DoclingEndpointBuilderFactory.java
@@ -807,6 +807,25 @@ public interface DoclingEndpointBuilderFactory {
doSetProperty("oauthProfile", oauthProfile);
return this;
}
+ /**
+ * When set, the output directory passed to the docling CLI must
resolve
+ * inside this directory once normalized. Applies to the
+ * CamelDoclingOutputFilePath header. The check is lexical and does not
+ * resolve symbolic links, matching inputBaseDirectory. When empty, no
+ * directory restriction is applied and the header value is only
+ * normalized.
+ *
+ * The option is a: <code>java.lang.String</code> type.
+ *
+ * Group: security
+ *
+ * @param outputBaseDirectory the value to set
+ * @return the dsl builder
+ */
+ default DoclingEndpointBuilder outputBaseDirectory(String
outputBaseDirectory) {
+ doSetProperty("outputBaseDirectory", outputBaseDirectory);
+ return this;
+ }
}
/**