This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a commit to branch camel-4.18.x
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/camel-4.18.x by this push:
new 54552df4b9e5 [backport camel-4.18.x] CAMEL-24894: camel-docling - make
output path handling consistent with input path and custom-argument handling
(#27362)
54552df4b9e5 is described below
commit 54552df4b9e512cb2025fad22b88429ef0e0169c
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Oct 5 09:53:02 2026 +0200
[backport camel-4.18.x] CAMEL-24894: camel-docling - make output path
handling consistent with input path and custom-argument handling (#27362)
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.
* 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.
* 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
* 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
---------
(cherry picked from commit 48485d408f190c46ef408ba4ba75e45b4e8ffb04)
[backport camel-4.18.x] Upgrade note moved to the 4.18.4 -> 4.18.5 section
of the 4.18 upgrade guide,
generated sources regenerated, test uses camel-test-junit5.
Signed-off-by: Claus Ibsen <[email protected]>
Co-authored-by: Andrea Cosentino <[email protected]>
Co-authored-by: Claude Opus 5.5 (1M context) <[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_18.adoc | 8 +
.../dsl/DoclingComponentBuilderFactory.java | 21 +++
.../dsl/DoclingEndpointBuilderFactory.java | 19 +++
14 files changed, 330 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 74e1c6949942..338a275c49dc 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
@@ -72,7 +72,8 @@
"authenticationScheme": { "index": 45, "kind": "property", "displayName":
"Authentication Scheme", "group": "security", "label": "security", "required":
false, "type": "enum", "javaType":
"org.apache.camel.component.docling.AuthenticationScheme", "enum": [ "NONE",
"BEARER", "API_KEY" ], "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "defaultValue": "NONE", "configurationClass":
"org.apache.camel.component.docling.DoclingConfiguration", "configuration [...]
"authenticationToken": { "index": 46, "kind": "property", "displayName":
"Authentication Token", "group": "security", "label": "security", "required":
false, "type": "string", "javaType": "java.lang.String", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": true,
"configurationClass":
"org.apache.camel.component.docling.DoclingConfiguration",
"configurationField": "configuration", "description": "Authentication token for
docling-serve API (Bearer token or API [...]
"inputBaseDirectory": { "index": 47, "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": 48, "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" }
+ "maxFileSize": { "index": 48, "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" },
+ "outputBaseDirectory": { "index": 49, "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" },
@@ -152,6 +153,7 @@
"authenticationScheme": { "index": 44, "kind": "parameter", "displayName":
"Authentication Scheme", "group": "security", "label": "security", "required":
false, "type": "enum", "javaType":
"org.apache.camel.component.docling.AuthenticationScheme", "enum": [ "NONE",
"BEARER", "API_KEY" ], "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "defaultValue": "NONE", "configurationClass":
"org.apache.camel.component.docling.DoclingConfiguration", "configuratio [...]
"authenticationToken": { "index": 45, "kind": "parameter", "displayName":
"Authentication Token", "group": "security", "label": "security", "required":
false, "type": "string", "javaType": "java.lang.String", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": true,
"configurationClass":
"org.apache.camel.component.docling.DoclingConfiguration",
"configurationField": "configuration", "description": "Authentication token for
docling-serve API (Bearer token or API [...]
"inputBaseDirectory": { "index": 46, "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": 47, "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" }
+ "maxFileSize": { "index": 47, "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" },
+ "outputBaseDirectory": { "index": 48, "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 0aef255d2be8..9f1bdbd12a72 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
@@ -481,7 +491,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 1c246c8fd60e..a229c19bedf8 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
@@ -106,6 +106,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":
@@ -208,6 +210,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":
@@ -311,6 +315,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 91ac3233bebe..fd85b059c961 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
@@ -94,6 +94,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":
@@ -191,6 +193,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":
@@ -289,6 +293,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 c17797ad91a8..49c0ac1cbb21 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
@@ -96,6 +96,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":
@@ -195,6 +197,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":
@@ -295,6 +299,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 c5b710b8c9a9..db2a85247546 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
@@ -23,7 +23,7 @@ public class DoclingEndpointUriFactory extends
org.apache.camel.support.componen
private static final Set<String> SECRET_PROPERTY_NAMES;
private static final Map<String, String> MULTI_VALUE_PREFIXES;
static {
- Set<String> props = new HashSet<>(48);
+ Set<String> props = new HashSet<>(49);
props.add("abortOnError");
props.add("allowFilePathSource");
props.add("allowUrlSource");
@@ -62,6 +62,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 74e1c6949942..338a275c49dc 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
@@ -72,7 +72,8 @@
"authenticationScheme": { "index": 45, "kind": "property", "displayName":
"Authentication Scheme", "group": "security", "label": "security", "required":
false, "type": "enum", "javaType":
"org.apache.camel.component.docling.AuthenticationScheme", "enum": [ "NONE",
"BEARER", "API_KEY" ], "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "defaultValue": "NONE", "configurationClass":
"org.apache.camel.component.docling.DoclingConfiguration", "configuration [...]
"authenticationToken": { "index": 46, "kind": "property", "displayName":
"Authentication Token", "group": "security", "label": "security", "required":
false, "type": "string", "javaType": "java.lang.String", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": true,
"configurationClass":
"org.apache.camel.component.docling.DoclingConfiguration",
"configurationField": "configuration", "description": "Authentication token for
docling-serve API (Bearer token or API [...]
"inputBaseDirectory": { "index": 47, "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": 48, "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" }
+ "maxFileSize": { "index": 48, "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" },
+ "outputBaseDirectory": { "index": 49, "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" },
@@ -152,6 +153,7 @@
"authenticationScheme": { "index": 44, "kind": "parameter", "displayName":
"Authentication Scheme", "group": "security", "label": "security", "required":
false, "type": "enum", "javaType":
"org.apache.camel.component.docling.AuthenticationScheme", "enum": [ "NONE",
"BEARER", "API_KEY" ], "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "defaultValue": "NONE", "configurationClass":
"org.apache.camel.component.docling.DoclingConfiguration", "configuratio [...]
"authenticationToken": { "index": 45, "kind": "parameter", "displayName":
"Authentication Token", "group": "security", "label": "security", "required":
false, "type": "string", "javaType": "java.lang.String", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": true,
"configurationClass":
"org.apache.camel.component.docling.DoclingConfiguration",
"configurationField": "configuration", "description": "Authentication token for
docling-serve API (Bearer token or API [...]
"inputBaseDirectory": { "index": 46, "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": 47, "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" }
+ "maxFileSize": { "index": 47, "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" },
+ "outputBaseDirectory": { "index": 48, "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 01ad2d76bd44..8c6e64f17009 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
@@ -144,6 +144,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
@@ -154,6 +159,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
@@ -467,7 +477,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 f833800024dc..9bf7b3acb8f8 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")
@@ -331,6 +339,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 f1b6c24d43f6..06880889638f 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
@@ -1838,7 +1838,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());
@@ -1999,17 +2001,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..492fc0f293f6
--- /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.junit5.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_18.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_18.adoc
index 8e8789842e2b..86e21b7a320a 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_18.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_18.adoc
@@ -13,6 +13,14 @@ See the xref:camel-upgrade-recipes-tool.adoc[documentation]
page for details.
== Upgrading from 4.18.4 to 4.18.5
+=== camel-docling - output path handling
+
+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-core - Rest DSL response Content-Type
Under json or xml binding, a response without a Content-Type header again
takes it from the `produces` of the rest
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 50df3b8a4993..19e698309760 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
@@ -891,6 +891,26 @@ public interface DoclingComponentBuilderFactory {
doSetProperty("maxFileSize", maxFileSize);
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
@@ -961,6 +981,7 @@ public interface DoclingComponentBuilderFactory {
case "authenticationToken":
getOrCreateConfiguration((DoclingComponent)
component).setAuthenticationToken((java.lang.String) value); return true;
case "inputBaseDirectory":
getOrCreateConfiguration((DoclingComponent)
component).setInputBaseDirectory((java.lang.String) value); return true;
case "maxFileSize": getOrCreateConfiguration((DoclingComponent)
component).setMaxFileSize((long) 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 574c25119f89..28c5be0877cf 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
@@ -653,6 +653,25 @@ public interface DoclingEndpointBuilderFactory {
doSetProperty("maxFileSize", maxFileSize);
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;
+ }
}
/**