This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch backport/CAMEL-24464-4.22.x in repository https://gitbox.apache.org/repos/asf/camel.git
commit 9da596385067a2bafd44fedd4114af6ecb939449 Author: Omar Atie <[email protected]> AuthorDate: Mon Aug 31 13:08:45 2026 -0700 CAMEL-24464: Clarify exec control headers require allowControlHeaders Since CAMEL-23315 (Camel 4.20), CamelExecCommand* in-headers only override URI options when allowControlHeaders=true, but the component docs still implied they always worked, which misled users on the default (false) setting. Update the @Metadata docs, generated catalog, and exec-component.adoc to state the allowControlHeaders prerequisite for all control headers. Log a once-per-endpoint WARN in DefaultExecBinding when control headers are present but ignored. Mark allowControlHeaders with security="insecure:dev" and register it in SecurityUtils so camel.main.profile=prod blocks the opt-in by default. Add a 4.23 upgrade-guide entry for camel-exec. Co-authored-by: Cursor Agent <[email protected]> Co-authored-by: Guillaume Nodet <[email protected]> Co-authored-by: Claude Sonnet 5 <[email protected]> Closes #25846 (cherry picked from commit 19e69f186fb715e198b30d1876e83bca7941a66a) --- .../org/apache/camel/catalog/components/exec.json | 20 +-- .../apache/camel/catalog/docs/exec-component.adoc | 41 +++++++ .../org/apache/camel/component/exec/exec.json | 20 +-- .../camel-exec/src/main/docs/exec-component.adoc | 41 +++++++ .../apache/camel/component/exec/ExecBinding.java | 28 +++-- .../apache/camel/component/exec/ExecComponent.java | 10 +- .../apache/camel/component/exec/ExecEndpoint.java | 10 +- .../component/exec/impl/DefaultExecBinding.java | 36 ++++++ .../component/exec/DefaultExecBindingTest.java | 136 ++++++++++++++++++--- .../camel/component/exec/ExecProducerTest.java | 19 +++ .../java/org/apache/camel/util/SecurityUtils.java | 1 + .../ROOT/pages/camel-4x-upgrade-guide-4_22.adoc | 12 ++ .../component/dsl/ExecComponentBuilderFactory.java | 12 +- .../endpoint/dsl/ExecEndpointBuilderFactory.java | 50 +++++--- 14 files changed, 364 insertions(+), 72 deletions(-) diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/exec.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/exec.json index ee2f679c33ac..79d5f450c64e 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/exec.json +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/exec.json @@ -27,22 +27,22 @@ "lazyStartProducer": { "index": 0, "kind": "property", "displayName": "Lazy Start Producer", "group": "producer", "label": "producer", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a producer may otherwise fail [...] "timeout": { "index": 1, "kind": "property", "displayName": "Timeout", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "long", "deprecated": false, "autowired": false, "secret": false, "description": "The timeout, in milliseconds, after which the executable should be terminated. If execution has not completed within the timeout, the component will send a termination request." }, "workingDir": { "index": 2, "kind": "property", "displayName": "Working Dir", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The directory in which the command should be executed. If null, the working directory of the current process will be used." }, - "allowControlHeaders": { "index": 3, "kind": "property", "displayName": "Allow Control Headers", "group": "advanced", "label": "advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether to allow to use Camel headers or not (default false). Enabling this allows to specify dynamic command line arguments via message header. However this can be seen as a potential securi [...] + "allowControlHeaders": { "index": 3, "kind": "property", "displayName": "Allow Control Headers", "group": "advanced", "label": "advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "security": "insecure:dev", "defaultValue": false, "description": "Whether {code CamelExec} in-headers may override URI options (default false since Camel 4.20). When false, CamelExecCommandExecutable, CamelExecCommandArgs, CamelE [...] "autowiredEnabled": { "index": 4, "kind": "property", "displayName": "Autowired Enabled", "group": "advanced", "label": "advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": true, "description": "Whether autowiring is enabled. This is used for automatic autowiring options (the option must be marked as autowired) by looking up in the registry to find if there is a single instance of matching t [...] "binding": { "index": 5, "kind": "property", "displayName": "Binding", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "org.apache.camel.component.exec.ExecBinding", "deprecated": false, "autowired": false, "secret": false, "description": "To use a custom org.apache.commons.exec.ExecBinding for advanced use-cases." }, "commandExecutor": { "index": 6, "kind": "property", "displayName": "Command Executor", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "org.apache.camel.component.exec.ExecCommandExecutor", "deprecated": false, "autowired": false, "secret": false, "description": "To use a custom org.apache.commons.exec.ExecCommandExecutor that customizes the command execution. The default command executor utilizes the commons-exec library, which adds a shut [...] }, "headers": { - "CamelExecCommandExecutable": { "index": 0, "kind": "header", "displayName": "", "group": "in", "label": "in", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The name of the system command that will be executed. Overrides executable in the URI.", "constantName": "org.apache.camel.component.exec.ExecBinding#EXEC_COMMAND_EXECUTABLE" }, - "CamelExecCommandArgs": { "index": 1, "kind": "header", "displayName": "", "group": "in", "label": "in", "required": false, "javaType": "java.util.List<String> or String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Command-line argument(s) to pass to the executed process. The argument(s) is\/are used literally - no quoting is applied. Overrides any existing args in the URI.", "constantName": "org.apache.camel.component.exec.ExecBi [...] - "CamelExecCommandOutFile": { "index": 2, "kind": "header", "displayName": "", "group": "in", "label": "in", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The name of a file, created by the executable, that should be considered as its output. Overrides any existing outFile in the URI.", "constantName": "org.apache.camel.component.exec.ExecBinding#EXEC_COMMAND_OUT_FILE" }, - "CamelExecCommandWorkingDir": { "index": 3, "kind": "header", "displayName": "", "group": "in", "label": "in", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The directory in which the command should be executed. Overrides any existing workingDir in the URI.", "constantName": "org.apache.camel.component.exec.ExecBinding#EXEC_COMMAND_WORKING_DIR" }, - "CamelExecCommandTimeout": { "index": 4, "kind": "header", "displayName": "", "group": "in", "label": "in", "required": false, "javaType": "long", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The timeout, in milliseconds, after which the executable should be terminated. Overrides any existing timeout in the URI.", "constantName": "org.apache.camel.component.exec.ExecBinding#EXEC_COMMAND_TIMEOUT" }, - "CamelExecExitValues": { "index": 5, "kind": "header", "displayName": "", "group": "in", "label": "in", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The exit values for successful execution of the process. Overrides any existing exitValues in the URI.", "constantName": "org.apache.camel.component.exec.ExecBinding#EXEC_COMMAND_EXIT_VALUES" }, + "CamelExecCommandExecutable": { "index": 0, "kind": "header", "displayName": "", "group": "in", "label": "in", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The name of the system command that will be executed. Overrides executable in the URI. Requires allowControlHeaders=true on the exec endpoint or component (default is false since Camel 4.20).", "constantName": "org.apache.camel.component.e [...] + "CamelExecCommandArgs": { "index": 1, "kind": "header", "displayName": "", "group": "in", "label": "in", "required": false, "javaType": "java.util.List<String> or String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Command-line argument(s) to pass to the executed process. The argument(s) is\/are used literally - no quoting is applied. Overrides any existing args in the URI. Requires allowControlHeaders=true on the exec endpoint or [...] + "CamelExecCommandOutFile": { "index": 2, "kind": "header", "displayName": "", "group": "in", "label": "in", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The name of a file, created by the executable, that should be considered as its output. Overrides any existing outFile in the URI. Requires allowControlHeaders=true on the exec endpoint or component (default is false since Camel 4.20).", "con [...] + "CamelExecCommandWorkingDir": { "index": 3, "kind": "header", "displayName": "", "group": "in", "label": "in", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The directory in which the command should be executed. Overrides any existing workingDir in the URI. Requires allowControlHeaders=true on the exec endpoint or component (default is false since Camel 4.20).", "constantName": "org.apache.cam [...] + "CamelExecCommandTimeout": { "index": 4, "kind": "header", "displayName": "", "group": "in", "label": "in", "required": false, "javaType": "long", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The timeout, in milliseconds, after which the executable should be terminated. Overrides any existing timeout in the URI. Requires allowControlHeaders=true on the exec endpoint or component (default is false since Camel 4.20).", "constantName": [...] + "CamelExecExitValues": { "index": 5, "kind": "header", "displayName": "", "group": "in", "label": "in", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The exit values for successful execution of the process. Overrides any existing exitValues in the URI. Requires allowControlHeaders=true on the exec endpoint or component (default is false since Camel 4.20).", "constantName": "org.apache.camel.co [...] "CamelExecStderr": { "index": 6, "kind": "header", "displayName": "", "group": "out", "label": "out", "required": false, "javaType": "java.io.InputStream", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The value of this header points to the standard error stream (stderr) of the executable. If no stderr is written, the value is null.", "constantName": "org.apache.camel.component.exec.ExecBinding#EXEC_STDERR" }, "CamelExecExitValue": { "index": 7, "kind": "header", "displayName": "", "group": "out", "label": "out", "required": false, "javaType": "int", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "important": true, "description": "The value of this header is the _exit value_ of the executable. Non-zero exit values typically indicate abnormal termination. Note that the exit value is OS-dependent.", "constantName": "org.apache.camel.component.exec.ExecBindin [...] - "CamelExecUseStderrOnEmptyStdout": { "index": 8, "kind": "header", "displayName": "", "group": "in", "label": "in", "required": false, "javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Indicates that when stdout is empty, this component will populate the Camel Message Body with stderr. This behavior is disabled (false) by default.", "constantName": "org.apache.camel.component.exec.ExecBinding#EXEC_USE_STDERR_ON_EMP [...] - "CamelExecCommandLogLevel": { "index": 9, "kind": "header", "displayName": "", "group": "in", "label": "in", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Logging level to be used for commands during execution. The default value is DEBUG. Possible values are TRACE, DEBUG, INFO, WARN, ERROR or OFF (Values of LoggingLevel enum)", "constantName": "org.apache.camel.component.exec.ExecBinding#EXEC_ [...] + "CamelExecUseStderrOnEmptyStdout": { "index": 8, "kind": "header", "displayName": "", "group": "in", "label": "in", "required": false, "javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Indicates that when stdout is empty, this component will populate the Camel Message Body with stderr. This behavior is disabled (false) by default. Requires allowControlHeaders=true on the exec endpoint or component (default is false [...] + "CamelExecCommandLogLevel": { "index": 9, "kind": "header", "displayName": "", "group": "in", "label": "in", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Logging level to be used for commands during execution. The default value is DEBUG. Possible values are TRACE, DEBUG, INFO, WARN, ERROR or OFF (Values of LoggingLevel enum). Requires allowControlHeaders=true on the exec endpoint or component [...] }, "properties": { "executable": { "index": 0, "kind": "path", "displayName": "Executable", "group": "producer", "label": "", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Sets the executable to be executed. The executable must not be empty or null." }, @@ -54,7 +54,7 @@ "useStderrOnEmptyStdout": { "index": 6, "kind": "parameter", "displayName": "Use Stderr On Empty Stdout", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "A boolean indicating that when stdout is empty, this component will populate the Camel Message Body with stderr. This behavior is disabled (false) by default." }, "workingDir": { "index": 7, "kind": "parameter", "displayName": "Working Dir", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The directory in which the command should be executed. If null, the working directory of the current process will be used." }, "lazyStartProducer": { "index": 8, "kind": "parameter", "displayName": "Lazy Start Producer", "group": "producer (advanced)", "label": "producer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a produc [...] - "allowControlHeaders": { "index": 9, "kind": "parameter", "displayName": "Allow Control Headers", "group": "advanced", "label": "advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether to allow to use Camel headers or not (default false). Enabling this allows to specify dynamic command line arguments via message header. However this can be seen as a potential secur [...] + "allowControlHeaders": { "index": 9, "kind": "parameter", "displayName": "Allow Control Headers", "group": "advanced", "label": "advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "security": "insecure:dev", "defaultValue": false, "description": "Whether {code CamelExec} in-headers may override URI options (default false since Camel 4.20). When false, CamelExecCommandExecutable, CamelExecCommandArgs, Camel [...] "binding": { "index": 10, "kind": "parameter", "displayName": "Binding", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "org.apache.camel.component.exec.ExecBinding", "deprecated": false, "autowired": false, "secret": false, "description": "To use a custom org.apache.commons.exec.ExecBinding for advanced use-cases." }, "commandExecutor": { "index": 11, "kind": "parameter", "displayName": "Command Executor", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "org.apache.camel.component.exec.ExecCommandExecutor", "deprecated": false, "autowired": false, "secret": false, "description": "To use a custom org.apache.commons.exec.ExecCommandExecutor that customizes the command execution. The default command executor utilizes the commons-exec library, which adds a sh [...] } diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/exec-component.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/exec-component.adoc index ad82f90cec84..64ca34aae1f8 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/exec-component.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/exec-component.adoc @@ -48,6 +48,47 @@ include::partial$component-endpoint-headers.adoc[] == Usage +=== Control headers + +Since Camel 4.20, the exec in-headers that override URI options (`CamelExecCommand*`, +`CamelExecExitValues`, and `CamelExecUseStderrOnEmptyStdout`) are disabled by default. This prevents untrusted message sources from redirecting +command execution. To use dynamic executable, arguments, working directory, or +other command settings from headers, enable the opt-in flag on the endpoint or +component: + +[tabs] +==== +Java:: ++ +[source,java] +---- +from("direct:run") + .setHeader("CamelExecCommandArgs", constant("ARGS-WORK")) + .to("exec:echo?allowControlHeaders=true"); +---- + +YAML:: ++ +[source,yaml] +---- +- route: + from: + uri: direct:run + steps: + - setHeader: + name: CamelExecCommandArgs + constant: "ARGS-WORK" + - to: + uri: exec:echo + parameters: + allowControlHeaders: true +---- +==== + +When `allowControlHeaders` is `false` (the default), those in-headers on the exchange +are ignored (they are not removed), and a WARN is logged once per exec endpoint. +URI parameters such as `args` continue to work as before. + === Message body If the component receives an `in` message body that is diff --git a/components/camel-exec/src/generated/resources/META-INF/org/apache/camel/component/exec/exec.json b/components/camel-exec/src/generated/resources/META-INF/org/apache/camel/component/exec/exec.json index ee2f679c33ac..79d5f450c64e 100644 --- a/components/camel-exec/src/generated/resources/META-INF/org/apache/camel/component/exec/exec.json +++ b/components/camel-exec/src/generated/resources/META-INF/org/apache/camel/component/exec/exec.json @@ -27,22 +27,22 @@ "lazyStartProducer": { "index": 0, "kind": "property", "displayName": "Lazy Start Producer", "group": "producer", "label": "producer", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a producer may otherwise fail [...] "timeout": { "index": 1, "kind": "property", "displayName": "Timeout", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "long", "deprecated": false, "autowired": false, "secret": false, "description": "The timeout, in milliseconds, after which the executable should be terminated. If execution has not completed within the timeout, the component will send a termination request." }, "workingDir": { "index": 2, "kind": "property", "displayName": "Working Dir", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The directory in which the command should be executed. If null, the working directory of the current process will be used." }, - "allowControlHeaders": { "index": 3, "kind": "property", "displayName": "Allow Control Headers", "group": "advanced", "label": "advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether to allow to use Camel headers or not (default false). Enabling this allows to specify dynamic command line arguments via message header. However this can be seen as a potential securi [...] + "allowControlHeaders": { "index": 3, "kind": "property", "displayName": "Allow Control Headers", "group": "advanced", "label": "advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "security": "insecure:dev", "defaultValue": false, "description": "Whether {code CamelExec} in-headers may override URI options (default false since Camel 4.20). When false, CamelExecCommandExecutable, CamelExecCommandArgs, CamelE [...] "autowiredEnabled": { "index": 4, "kind": "property", "displayName": "Autowired Enabled", "group": "advanced", "label": "advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": true, "description": "Whether autowiring is enabled. This is used for automatic autowiring options (the option must be marked as autowired) by looking up in the registry to find if there is a single instance of matching t [...] "binding": { "index": 5, "kind": "property", "displayName": "Binding", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "org.apache.camel.component.exec.ExecBinding", "deprecated": false, "autowired": false, "secret": false, "description": "To use a custom org.apache.commons.exec.ExecBinding for advanced use-cases." }, "commandExecutor": { "index": 6, "kind": "property", "displayName": "Command Executor", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "org.apache.camel.component.exec.ExecCommandExecutor", "deprecated": false, "autowired": false, "secret": false, "description": "To use a custom org.apache.commons.exec.ExecCommandExecutor that customizes the command execution. The default command executor utilizes the commons-exec library, which adds a shut [...] }, "headers": { - "CamelExecCommandExecutable": { "index": 0, "kind": "header", "displayName": "", "group": "in", "label": "in", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The name of the system command that will be executed. Overrides executable in the URI.", "constantName": "org.apache.camel.component.exec.ExecBinding#EXEC_COMMAND_EXECUTABLE" }, - "CamelExecCommandArgs": { "index": 1, "kind": "header", "displayName": "", "group": "in", "label": "in", "required": false, "javaType": "java.util.List<String> or String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Command-line argument(s) to pass to the executed process. The argument(s) is\/are used literally - no quoting is applied. Overrides any existing args in the URI.", "constantName": "org.apache.camel.component.exec.ExecBi [...] - "CamelExecCommandOutFile": { "index": 2, "kind": "header", "displayName": "", "group": "in", "label": "in", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The name of a file, created by the executable, that should be considered as its output. Overrides any existing outFile in the URI.", "constantName": "org.apache.camel.component.exec.ExecBinding#EXEC_COMMAND_OUT_FILE" }, - "CamelExecCommandWorkingDir": { "index": 3, "kind": "header", "displayName": "", "group": "in", "label": "in", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The directory in which the command should be executed. Overrides any existing workingDir in the URI.", "constantName": "org.apache.camel.component.exec.ExecBinding#EXEC_COMMAND_WORKING_DIR" }, - "CamelExecCommandTimeout": { "index": 4, "kind": "header", "displayName": "", "group": "in", "label": "in", "required": false, "javaType": "long", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The timeout, in milliseconds, after which the executable should be terminated. Overrides any existing timeout in the URI.", "constantName": "org.apache.camel.component.exec.ExecBinding#EXEC_COMMAND_TIMEOUT" }, - "CamelExecExitValues": { "index": 5, "kind": "header", "displayName": "", "group": "in", "label": "in", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The exit values for successful execution of the process. Overrides any existing exitValues in the URI.", "constantName": "org.apache.camel.component.exec.ExecBinding#EXEC_COMMAND_EXIT_VALUES" }, + "CamelExecCommandExecutable": { "index": 0, "kind": "header", "displayName": "", "group": "in", "label": "in", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The name of the system command that will be executed. Overrides executable in the URI. Requires allowControlHeaders=true on the exec endpoint or component (default is false since Camel 4.20).", "constantName": "org.apache.camel.component.e [...] + "CamelExecCommandArgs": { "index": 1, "kind": "header", "displayName": "", "group": "in", "label": "in", "required": false, "javaType": "java.util.List<String> or String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Command-line argument(s) to pass to the executed process. The argument(s) is\/are used literally - no quoting is applied. Overrides any existing args in the URI. Requires allowControlHeaders=true on the exec endpoint or [...] + "CamelExecCommandOutFile": { "index": 2, "kind": "header", "displayName": "", "group": "in", "label": "in", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The name of a file, created by the executable, that should be considered as its output. Overrides any existing outFile in the URI. Requires allowControlHeaders=true on the exec endpoint or component (default is false since Camel 4.20).", "con [...] + "CamelExecCommandWorkingDir": { "index": 3, "kind": "header", "displayName": "", "group": "in", "label": "in", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The directory in which the command should be executed. Overrides any existing workingDir in the URI. Requires allowControlHeaders=true on the exec endpoint or component (default is false since Camel 4.20).", "constantName": "org.apache.cam [...] + "CamelExecCommandTimeout": { "index": 4, "kind": "header", "displayName": "", "group": "in", "label": "in", "required": false, "javaType": "long", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The timeout, in milliseconds, after which the executable should be terminated. Overrides any existing timeout in the URI. Requires allowControlHeaders=true on the exec endpoint or component (default is false since Camel 4.20).", "constantName": [...] + "CamelExecExitValues": { "index": 5, "kind": "header", "displayName": "", "group": "in", "label": "in", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The exit values for successful execution of the process. Overrides any existing exitValues in the URI. Requires allowControlHeaders=true on the exec endpoint or component (default is false since Camel 4.20).", "constantName": "org.apache.camel.co [...] "CamelExecStderr": { "index": 6, "kind": "header", "displayName": "", "group": "out", "label": "out", "required": false, "javaType": "java.io.InputStream", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The value of this header points to the standard error stream (stderr) of the executable. If no stderr is written, the value is null.", "constantName": "org.apache.camel.component.exec.ExecBinding#EXEC_STDERR" }, "CamelExecExitValue": { "index": 7, "kind": "header", "displayName": "", "group": "out", "label": "out", "required": false, "javaType": "int", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "important": true, "description": "The value of this header is the _exit value_ of the executable. Non-zero exit values typically indicate abnormal termination. Note that the exit value is OS-dependent.", "constantName": "org.apache.camel.component.exec.ExecBindin [...] - "CamelExecUseStderrOnEmptyStdout": { "index": 8, "kind": "header", "displayName": "", "group": "in", "label": "in", "required": false, "javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Indicates that when stdout is empty, this component will populate the Camel Message Body with stderr. This behavior is disabled (false) by default.", "constantName": "org.apache.camel.component.exec.ExecBinding#EXEC_USE_STDERR_ON_EMP [...] - "CamelExecCommandLogLevel": { "index": 9, "kind": "header", "displayName": "", "group": "in", "label": "in", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Logging level to be used for commands during execution. The default value is DEBUG. Possible values are TRACE, DEBUG, INFO, WARN, ERROR or OFF (Values of LoggingLevel enum)", "constantName": "org.apache.camel.component.exec.ExecBinding#EXEC_ [...] + "CamelExecUseStderrOnEmptyStdout": { "index": 8, "kind": "header", "displayName": "", "group": "in", "label": "in", "required": false, "javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Indicates that when stdout is empty, this component will populate the Camel Message Body with stderr. This behavior is disabled (false) by default. Requires allowControlHeaders=true on the exec endpoint or component (default is false [...] + "CamelExecCommandLogLevel": { "index": 9, "kind": "header", "displayName": "", "group": "in", "label": "in", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Logging level to be used for commands during execution. The default value is DEBUG. Possible values are TRACE, DEBUG, INFO, WARN, ERROR or OFF (Values of LoggingLevel enum). Requires allowControlHeaders=true on the exec endpoint or component [...] }, "properties": { "executable": { "index": 0, "kind": "path", "displayName": "Executable", "group": "producer", "label": "", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Sets the executable to be executed. The executable must not be empty or null." }, @@ -54,7 +54,7 @@ "useStderrOnEmptyStdout": { "index": 6, "kind": "parameter", "displayName": "Use Stderr On Empty Stdout", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "A boolean indicating that when stdout is empty, this component will populate the Camel Message Body with stderr. This behavior is disabled (false) by default." }, "workingDir": { "index": 7, "kind": "parameter", "displayName": "Working Dir", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The directory in which the command should be executed. If null, the working directory of the current process will be used." }, "lazyStartProducer": { "index": 8, "kind": "parameter", "displayName": "Lazy Start Producer", "group": "producer (advanced)", "label": "producer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a produc [...] - "allowControlHeaders": { "index": 9, "kind": "parameter", "displayName": "Allow Control Headers", "group": "advanced", "label": "advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether to allow to use Camel headers or not (default false). Enabling this allows to specify dynamic command line arguments via message header. However this can be seen as a potential secur [...] + "allowControlHeaders": { "index": 9, "kind": "parameter", "displayName": "Allow Control Headers", "group": "advanced", "label": "advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "security": "insecure:dev", "defaultValue": false, "description": "Whether {code CamelExec} in-headers may override URI options (default false since Camel 4.20). When false, CamelExecCommandExecutable, CamelExecCommandArgs, Camel [...] "binding": { "index": 10, "kind": "parameter", "displayName": "Binding", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "org.apache.camel.component.exec.ExecBinding", "deprecated": false, "autowired": false, "secret": false, "description": "To use a custom org.apache.commons.exec.ExecBinding for advanced use-cases." }, "commandExecutor": { "index": 11, "kind": "parameter", "displayName": "Command Executor", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "org.apache.camel.component.exec.ExecCommandExecutor", "deprecated": false, "autowired": false, "secret": false, "description": "To use a custom org.apache.commons.exec.ExecCommandExecutor that customizes the command execution. The default command executor utilizes the commons-exec library, which adds a sh [...] } diff --git a/components/camel-exec/src/main/docs/exec-component.adoc b/components/camel-exec/src/main/docs/exec-component.adoc index ad82f90cec84..64ca34aae1f8 100644 --- a/components/camel-exec/src/main/docs/exec-component.adoc +++ b/components/camel-exec/src/main/docs/exec-component.adoc @@ -48,6 +48,47 @@ include::partial$component-endpoint-headers.adoc[] == Usage +=== Control headers + +Since Camel 4.20, the exec in-headers that override URI options (`CamelExecCommand*`, +`CamelExecExitValues`, and `CamelExecUseStderrOnEmptyStdout`) are disabled by default. This prevents untrusted message sources from redirecting +command execution. To use dynamic executable, arguments, working directory, or +other command settings from headers, enable the opt-in flag on the endpoint or +component: + +[tabs] +==== +Java:: ++ +[source,java] +---- +from("direct:run") + .setHeader("CamelExecCommandArgs", constant("ARGS-WORK")) + .to("exec:echo?allowControlHeaders=true"); +---- + +YAML:: ++ +[source,yaml] +---- +- route: + from: + uri: direct:run + steps: + - setHeader: + name: CamelExecCommandArgs + constant: "ARGS-WORK" + - to: + uri: exec:echo + parameters: + allowControlHeaders: true +---- +==== + +When `allowControlHeaders` is `false` (the default), those in-headers on the exchange +are ignored (they are not removed), and a WARN is logged once per exec endpoint. +URI parameters such as `args` continue to work as before. + === Message body If the component receives an `in` message body that is diff --git a/components/camel-exec/src/main/java/org/apache/camel/component/exec/ExecBinding.java b/components/camel-exec/src/main/java/org/apache/camel/component/exec/ExecBinding.java index 449a3bf4272a..8801cf4cbe83 100644 --- a/components/camel-exec/src/main/java/org/apache/camel/component/exec/ExecBinding.java +++ b/components/camel-exec/src/main/java/org/apache/camel/component/exec/ExecBinding.java @@ -36,7 +36,8 @@ public interface ExecBinding { */ @Metadata(label = "in", description = """ The name of the system command that will be executed. Overrides - `executable` in the URI.""", + `executable` in the URI. Requires `allowControlHeaders=true` on the exec + endpoint or component (default is `false` since Camel 4.20).""", javaType = "String") String EXEC_COMMAND_EXECUTABLE = "CamelExecCommandExecutable"; @@ -49,7 +50,8 @@ public interface ExecBinding { @Metadata(label = "in", description = """ Command-line argument(s) to pass to the executed process. The argument(s) is/are used literally - no quoting is applied. Overrides any existing - `args` in the URI.""", + `args` in the URI. Requires `allowControlHeaders=true` on the exec endpoint + or component (default is `false` since Camel 4.20).""", javaType = "java.util.List<String> or String") String EXEC_COMMAND_ARGS = "CamelExecCommandArgs"; @@ -61,7 +63,9 @@ public interface ExecBinding { */ @Metadata(label = "in", description = """ The name of a file, created by the executable, that should be considered - as its output. Overrides any existing `outFile` in the URI.""", + as its output. Overrides any existing `outFile` in the URI. Requires + `allowControlHeaders=true` on the exec endpoint or component (default is + `false` since Camel 4.20).""", javaType = "String") String EXEC_COMMAND_OUT_FILE = "CamelExecCommandOutFile"; @@ -72,7 +76,8 @@ public interface ExecBinding { */ @Metadata(label = "in", description = """ The directory in which the command should be executed. Overrides any - existing `workingDir` in the URI.""", + existing `workingDir` in the URI. Requires `allowControlHeaders=true` on + the exec endpoint or component (default is `false` since Camel 4.20).""", javaType = "String") String EXEC_COMMAND_WORKING_DIR = "CamelExecCommandWorkingDir"; @@ -82,7 +87,9 @@ public interface ExecBinding { */ @Metadata(label = "in", description = """ The timeout, in milliseconds, after which the executable should be - terminated. Overrides any existing `timeout` in the URI.""", + terminated. Overrides any existing `timeout` in the URI. Requires + `allowControlHeaders=true` on the exec endpoint or component (default is + `false` since Camel 4.20).""", javaType = "long") String EXEC_COMMAND_TIMEOUT = "CamelExecCommandTimeout"; @@ -93,7 +100,9 @@ public interface ExecBinding { */ @Metadata(label = "in", description = """ The exit values for successful execution of the process. - Overrides any existing `exitValues` in the URI.""", + Overrides any existing `exitValues` in the URI. Requires + `allowControlHeaders=true` on the exec endpoint or component (default is + `false` since Camel 4.20).""", javaType = "String") String EXEC_COMMAND_EXIT_VALUES = "CamelExecExitValues"; @@ -124,7 +133,8 @@ public interface ExecBinding { @Metadata(label = "in", description = """ Indicates that when `stdout` is empty, this component will populate the Camel Message Body with `stderr`. This behavior is disabled (`false`) by - default.""", + default. Requires `allowControlHeaders=true` on the exec endpoint or + component (default is `false` since Camel 4.20).""", javaType = "boolean") String EXEC_USE_STDERR_ON_EMPTY_STDOUT = "CamelExecUseStderrOnEmptyStdout"; @@ -135,7 +145,9 @@ public interface ExecBinding { @Metadata(label = "in", description = """ Logging level to be used for commands during execution. The default value is DEBUG. - Possible values are TRACE, DEBUG, INFO, WARN, ERROR or OFF (Values of LoggingLevel enum)""", + Possible values are TRACE, DEBUG, INFO, WARN, ERROR or OFF (Values of LoggingLevel enum). + Requires `allowControlHeaders=true` on the exec endpoint or component (default is `false` + since Camel 4.20).""", javaType = "String") String EXEC_COMMAND_LOG_LEVEL = "CamelExecCommandLogLevel"; diff --git a/components/camel-exec/src/main/java/org/apache/camel/component/exec/ExecComponent.java b/components/camel-exec/src/main/java/org/apache/camel/component/exec/ExecComponent.java index dc1fb8058ec6..11bc8519c0e1 100644 --- a/components/camel-exec/src/main/java/org/apache/camel/component/exec/ExecComponent.java +++ b/components/camel-exec/src/main/java/org/apache/camel/component/exec/ExecComponent.java @@ -37,7 +37,7 @@ public class ExecComponent extends DefaultComponent { @Metadata private String workingDir; - @Metadata(label = "advanced") + @Metadata(label = "advanced", security = "insecure:dev") private boolean allowControlHeaders; @Metadata(label = "advanced") private ExecCommandExecutor commandExecutor; @@ -102,9 +102,11 @@ public class ExecComponent extends DefaultComponent { } /** - * Whether to allow to use Camel headers or not (default false). Enabling this allows to specify dynamic command - * line arguments via message header. However this can be seen as a potential security vulnerability if the header - * is coming from a malicious user, so use this with care. + * Whether {@code CamelExec*} in-headers may override URI options (default {@code false} since Camel 4.20). When + * {@code false}, {@code CamelExecCommandExecutable}, {@code CamelExecCommandArgs}, {@code CamelExecCommandOutFile}, + * {@code CamelExecCommandWorkingDir}, {@code CamelExecCommandTimeout}, {@code CamelExecExitValues}, + * {@code CamelExecUseStderrOnEmptyStdout}, and {@code CamelExecCommandLogLevel} are ignored. Enable only when those + * headers come from a trusted route, not from an untrusted consumer. */ public void setAllowControlHeaders(boolean allowControlHeaders) { this.allowControlHeaders = allowControlHeaders; diff --git a/components/camel-exec/src/main/java/org/apache/camel/component/exec/ExecEndpoint.java b/components/camel-exec/src/main/java/org/apache/camel/component/exec/ExecEndpoint.java index dbe3c6edc276..4339c2304e5b 100644 --- a/components/camel-exec/src/main/java/org/apache/camel/component/exec/ExecEndpoint.java +++ b/components/camel-exec/src/main/java/org/apache/camel/component/exec/ExecEndpoint.java @@ -61,7 +61,7 @@ public class ExecEndpoint extends DefaultEndpoint { private boolean useStderrOnEmptyStdout; @UriParam(defaultValue = "DEBUG") private LoggingLevel commandLogLevel = LoggingLevel.DEBUG; - @UriParam(label = "advanced") + @UriParam(label = "advanced", security = "insecure:dev") private boolean allowControlHeaders; public ExecEndpoint(String uri, ExecComponent component) { @@ -211,9 +211,11 @@ public class ExecEndpoint extends DefaultEndpoint { } /** - * Whether to allow to use Camel headers or not (default false). Enabling this allows to specify dynamic command - * line arguments via message header. However this can be seen as a potential security vulnerability if the header - * is coming from a malicious user, so use this with care. + * Whether {@code CamelExec*} in-headers may override URI options (default {@code false} since Camel 4.20). When + * {@code false}, {@code CamelExecCommandExecutable}, {@code CamelExecCommandArgs}, {@code CamelExecCommandOutFile}, + * {@code CamelExecCommandWorkingDir}, {@code CamelExecCommandTimeout}, {@code CamelExecExitValues}, + * {@code CamelExecUseStderrOnEmptyStdout}, and {@code CamelExecCommandLogLevel} are ignored. Enable only when those + * headers come from a trusted route, not from an untrusted consumer. */ public void setAllowControlHeaders(boolean allowControlHeaders) { this.allowControlHeaders = allowControlHeaders; diff --git a/components/camel-exec/src/main/java/org/apache/camel/component/exec/impl/DefaultExecBinding.java b/components/camel-exec/src/main/java/org/apache/camel/component/exec/impl/DefaultExecBinding.java index b8905e073536..df10f7889406 100644 --- a/components/camel-exec/src/main/java/org/apache/camel/component/exec/impl/DefaultExecBinding.java +++ b/components/camel-exec/src/main/java/org/apache/camel/component/exec/impl/DefaultExecBinding.java @@ -18,9 +18,11 @@ package org.apache.camel.component.exec.impl; import java.io.File; import java.io.InputStream; +import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import org.apache.camel.Exchange; import org.apache.camel.LoggingLevel; @@ -45,12 +47,26 @@ public class DefaultExecBinding implements ExecBinding { private static final Logger LOG = LoggerFactory.getLogger(DefaultExecBinding.class); + private static final String[] CONTROL_HEADERS = { + EXEC_COMMAND_EXECUTABLE, + EXEC_COMMAND_ARGS, + EXEC_COMMAND_OUT_FILE, + EXEC_COMMAND_WORKING_DIR, + EXEC_COMMAND_TIMEOUT, + EXEC_COMMAND_EXIT_VALUES, + EXEC_USE_STDERR_ON_EMPTY_STDOUT, + EXEC_COMMAND_LOG_LEVEL + }; + + private final Set<String> ignoredControlHeadersWarnedEndpoints = ConcurrentHashMap.newKeySet(); + @Override @SuppressWarnings("unchecked") public ExecCommand readInput(Exchange exchange, ExecEndpoint endpoint) { ObjectHelper.notNull(exchange, "exchange"); ObjectHelper.notNull(endpoint, "endpoint"); + warnIgnoredControlHeaders(exchange, endpoint); // do not convert args as we do that manually later Object args = endpoint.isAllowControlHeaders() ? exchange.getIn().removeHeader(EXEC_COMMAND_ARGS) : null; String cmd = getAndRemoveHeader(endpoint, exchange.getIn(), EXEC_COMMAND_EXECUTABLE, endpoint.getExecutable(), @@ -153,4 +169,24 @@ public class DefaultExecBinding implements ExecBinding { } return h; } + + private void warnIgnoredControlHeaders(Exchange exchange, ExecEndpoint endpoint) { + if (endpoint.isAllowControlHeaders()) { + return; + } + Message in = exchange.getIn(); + List<String> ignored = new ArrayList<>(); + for (String headerName : CONTROL_HEADERS) { + if (in.getHeader(headerName) != null) { + ignored.add(headerName); + } + } + if (!ignored.isEmpty() && ignoredControlHeadersWarnedEndpoints.add(endpoint.getEndpointUri())) { + LOG.warn( + "Control header(s) {} are set but ignored because allowControlHeaders=false on {}. " + + "Set allowControlHeaders=true on the exec endpoint or component to enable dynamic command configuration. " + + "This warning is logged only once per exec endpoint.", + ignored, endpoint); + } + } } diff --git a/components/camel-exec/src/test/java/org/apache/camel/component/exec/DefaultExecBindingTest.java b/components/camel-exec/src/test/java/org/apache/camel/component/exec/DefaultExecBindingTest.java index 6d85a2d4d446..83ea91058968 100644 --- a/components/camel-exec/src/test/java/org/apache/camel/component/exec/DefaultExecBindingTest.java +++ b/components/camel-exec/src/test/java/org/apache/camel/component/exec/DefaultExecBindingTest.java @@ -19,42 +19,150 @@ package org.apache.camel.component.exec; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; import org.apache.camel.Exchange; import org.apache.camel.component.exec.impl.DefaultExecBinding; import org.apache.camel.test.junit6.CamelTestSupport; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.Logger; +import org.apache.logging.log4j.core.appender.AbstractAppender; +import org.apache.logging.log4j.core.config.Property; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; -public class DefaultExecBindingTest extends CamelTestSupport { +class DefaultExecBindingTest extends CamelTestSupport { @Test - public void testReadInput() throws Exception { - ExecCommand command = readInput("exec:test", Collections.EMPTY_LIST); - assertEquals(Collections.EMPTY_LIST, command.getArgs(), "Get a wrong args."); + void shouldReadArgsFromHeaderWhenControlHeadersEnabled() throws Exception { List<String> args = Arrays.asList("arg1", "arg2"); - command = readInput("exec:test", args); - assertEquals(args, command.getArgs(), "Get a wrong args."); + ExecCommand command = readInput("exec:test", args, true); + assertEquals(args, command.getArgs()); - command = readInput("exec:test", "arg1 arg2"); - assertEquals(args, command.getArgs(), "Get a wrong args."); + command = readInput("exec:test", "arg1 arg2", true); + assertEquals(args, command.getArgs()); - command = readInput("exec:test?args=arg1 arg2", null); - assertEquals(args, command.getArgs(), "Get a wrong args."); + command = readInput("exec:test?args=arg1 arg2", null, true); + assertEquals(args, command.getArgs()); + + command = readInput("exec:test", Collections.emptyList(), true); + assertEquals(Collections.emptyList(), command.getArgs()); + } + + @Test + void shouldIgnoreControlHeadersByDefault() throws Exception { + DefaultExecBinding binding = new DefaultExecBinding(); + ExecEndpoint execEndpoint = createExecEndpoint("exec:hostname", false); + Exchange exchange = execEndpoint.createExchange(); + exchange.getIn().setHeader(ExecBinding.EXEC_COMMAND_EXECUTABLE, "whoami"); + exchange.getIn().setHeader(ExecBinding.EXEC_COMMAND_ARGS, "ARGS-WORK"); + + ExecCommand command = binding.readInput(exchange, execEndpoint); + + assertEquals("hostname", command.getExecutable()); + assertEquals(Collections.emptyList(), command.getArgs()); + assertEquals("whoami", exchange.getIn().getHeader(ExecBinding.EXEC_COMMAND_EXECUTABLE)); + assertEquals("ARGS-WORK", exchange.getIn().getHeader(ExecBinding.EXEC_COMMAND_ARGS)); + } + + @Test + void shouldApplyControlHeadersWhenEnabled() throws Exception { + DefaultExecBinding binding = new DefaultExecBinding(); + ExecEndpoint execEndpoint = createExecEndpoint("exec:hostname", true); + Exchange exchange = execEndpoint.createExchange(); + exchange.getIn().setHeader(ExecBinding.EXEC_COMMAND_EXECUTABLE, "whoami"); + exchange.getIn().setHeader(ExecBinding.EXEC_COMMAND_ARGS, "ARGS-WORK"); + + ExecCommand command = binding.readInput(exchange, execEndpoint); + + assertEquals("whoami", command.getExecutable()); + assertEquals(List.of("ARGS-WORK"), command.getArgs()); + assertNull(exchange.getIn().getHeader(ExecBinding.EXEC_COMMAND_EXECUTABLE)); + assertNull(exchange.getIn().getHeader(ExecBinding.EXEC_COMMAND_ARGS)); + } + + @Test + void shouldKeepUriArgsWhenControlHeadersDisabled() throws Exception { + ExecCommand command = readInput("exec:echo?args=URIARGS-WORK", "ARGS-WORK", false); + + assertEquals(List.of("URIARGS-WORK"), command.getArgs()); + assertEquals("echo", command.getExecutable()); + } + + @Test + void shouldWarnOncePerEndpointWhenControlHeadersIgnored() throws Exception { + List<String> warnings = new CopyOnWriteArrayList<>(); + AbstractAppender appender = new AbstractAppender("CaptureWarn", null, null, true, Property.EMPTY_ARRAY) { + @Override + public void append(LogEvent event) { + if (event.getLevel() == Level.WARN + && event.getMessage().getFormattedMessage().contains("Control header")) { + warnings.add(event.getMessage().getFormattedMessage()); + } + } + }; + appender.start(); + Logger logger = (Logger) LogManager.getLogger(DefaultExecBinding.class); + logger.addAppender(appender); + + try { + DefaultExecBinding binding = new DefaultExecBinding(); + ExecComponent component = context.getComponent("exec", ExecComponent.class); + component.setAllowControlHeaders(false); + component.setBinding(binding); + + ExecEndpoint endpoint1 = (ExecEndpoint) component.createEndpoint("exec:hostname"); + ExecEndpoint endpoint2 = (ExecEndpoint) component.createEndpoint("exec:echo"); + + Exchange exchange1 = endpoint1.createExchange(); + exchange1.getIn().setHeader(ExecBinding.EXEC_COMMAND_EXECUTABLE, "whoami"); + binding.readInput(exchange1, endpoint1); + + Exchange exchange2 = endpoint2.createExchange(); + exchange2.getIn().setHeader(ExecBinding.EXEC_COMMAND_EXECUTABLE, "whoami"); + binding.readInput(exchange2, endpoint2); + + Exchange exchange3 = endpoint1.createExchange(); + exchange3.getIn().setHeader(ExecBinding.EXEC_COMMAND_EXECUTABLE, "whoami"); + binding.readInput(exchange3, endpoint1); + + assertEquals(2, warnings.size(), "Expected one warning per distinct exec endpoint"); + } finally { + logger.removeAppender(appender); + appender.stop(); + } + } + + @Test + void shouldApplyControlHeadersFromEndpointOption() throws Exception { + DefaultExecBinding binding = new DefaultExecBinding(); + ExecComponent component = context.getComponent("exec", ExecComponent.class); + component.setAllowControlHeaders(false); + ExecEndpoint execEndpoint = (ExecEndpoint) component.createEndpoint("exec:hostname?allowControlHeaders=true"); + Exchange exchange = execEndpoint.createExchange(); + exchange.getIn().setHeader(ExecBinding.EXEC_COMMAND_EXECUTABLE, "whoami"); + + ExecCommand command = binding.readInput(exchange, execEndpoint); + + assertEquals("whoami", command.getExecutable()); + assertNull(exchange.getIn().getHeader(ExecBinding.EXEC_COMMAND_EXECUTABLE)); } - private ExecCommand readInput(String execEndpointUri, Object args) throws Exception { + private ExecCommand readInput(String execEndpointUri, Object args, boolean allowControlHeaders) throws Exception { DefaultExecBinding binding = new DefaultExecBinding(); - ExecEndpoint execEndpoint = createExecEndpoint(execEndpointUri); + ExecEndpoint execEndpoint = createExecEndpoint(execEndpointUri, allowControlHeaders); Exchange exchange = execEndpoint.createExchange(); exchange.getIn().setHeader(ExecBinding.EXEC_COMMAND_ARGS, args); return binding.readInput(exchange, execEndpoint); } - private ExecEndpoint createExecEndpoint(String uri) throws Exception { + private ExecEndpoint createExecEndpoint(String uri, boolean allowControlHeaders) throws Exception { ExecComponent component = context.getComponent("exec", ExecComponent.class); - component.setAllowControlHeaders(true); + component.setAllowControlHeaders(allowControlHeaders); return (ExecEndpoint) component.createEndpoint(uri); } diff --git a/components/camel-exec/src/test/java/org/apache/camel/component/exec/ExecProducerTest.java b/components/camel-exec/src/test/java/org/apache/camel/component/exec/ExecProducerTest.java index fb77c129d2d4..fdc886f06e64 100644 --- a/components/camel-exec/src/test/java/org/apache/camel/component/exec/ExecProducerTest.java +++ b/components/camel-exec/src/test/java/org/apache/camel/component/exec/ExecProducerTest.java @@ -67,6 +67,25 @@ public class ExecProducerTest { assertEquals("mockedByCommandExecutorMock.exe", execCommandExecutorMock.lastCommandResult.getCommand().getExecutable()); } + @Test + @DirtiesContext + public void testIgnoreControlHeadersByDefault() { + final String command = "java"; + + producerTemplate.send(new Processor() { + + public void process(Exchange exchange) { + exchange.getIn().setBody("noinput"); + exchange.getIn().setHeader(EXEC_COMMAND_EXECUTABLE, command); + exchange.getIn().setHeader(EXEC_COMMAND_ARGS, Arrays.asList("-version")); + } + }); + + assertEquals("mockedByCommandExecutorMock.exe", + execCommandExecutorMock.lastCommandResult.getCommand().getExecutable()); + assertTrue(execCommandExecutorMock.lastCommandResult.getCommand().getArgs().isEmpty()); + } + @Test @DirtiesContext public void testOverrideExecutable() { diff --git a/core/camel-util/src/main/java/org/apache/camel/util/SecurityUtils.java b/core/camel-util/src/main/java/org/apache/camel/util/SecurityUtils.java index f9a474d827fc..22da8e66497e 100644 --- a/core/camel-util/src/main/java/org/apache/camel/util/SecurityUtils.java +++ b/core/camel-util/src/main/java/org/apache/camel/util/SecurityUtils.java @@ -57,6 +57,7 @@ public final class SecurityUtils { Map<String, SecurityOption> map = new HashMap<>(); // Generated by camel build tools - do NOT edit this map! // SECURITY-OPTIONS: START + map.put("allowcontrolheaders", new SecurityOption(INSECURE_DEV, "true")); map.put("allowfilepathsource", new SecurityOption(INSECURE_DEV, "true")); map.put("allowjavaserializedobject", new SecurityOption(INSECURE_SERIALIZATION, "true")); map.put("allowlocalwebhookurls", new SecurityOption(INSECURE_DEV, "true")); diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc index e6bb96e52b7b..625ae75e2620 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc @@ -33,6 +33,18 @@ The same default is now also applied to the `ClientConfig` that Camel builds for endpoints, when neither a referenced `ClientConfig` nor `hazelcastConfigUri` is supplied. Client mode previously behaved differently from node mode for an otherwise identical endpoint configuration. +=== camel-exec + +`allowControlHeaders` is now annotated `security = "insecure:dev"`. +With `camel.main.profile = prod` the default policy for that category is `fail`, +so an endpoint or component that sets `allowControlHeaders=true` will not start +unless you relax `camel.security.insecureDevPolicy`. + +When the flag is `false` (the default), any remaining `CamelExecCommand*`, +`CamelExecExitValues`, or `CamelExecUseStderrOnEmptyStdout` headers are ignored +and a WARN is logged once per exec endpoint. Those headers never overrode the +URI without the flag; they were just silent before. + == Upgrading Camel 4.21 to 4.22 === camel-tika diff --git a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/ExecComponentBuilderFactory.java b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/ExecComponentBuilderFactory.java index 5768d84bf5c4..61c316b92cab 100644 --- a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/ExecComponentBuilderFactory.java +++ b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/ExecComponentBuilderFactory.java @@ -110,11 +110,13 @@ public interface ExecComponentBuilderFactory { /** - * Whether to allow to use Camel headers or not (default false). - * Enabling this allows to specify dynamic command line arguments via - * message header. However this can be seen as a potential security - * vulnerability if the header is coming from a malicious user, so use - * this with care. + * Whether {code CamelExec} in-headers may override URI options (default + * false since Camel 4.20). When false, CamelExecCommandExecutable, + * CamelExecCommandArgs, CamelExecCommandOutFile, + * CamelExecCommandWorkingDir, CamelExecCommandTimeout, + * CamelExecExitValues, CamelExecUseStderrOnEmptyStdout, and + * CamelExecCommandLogLevel are ignored. Enable only when those headers + * come from a trusted route, not from an untrusted consumer. * * The option is a: <code>boolean</code> type. * diff --git a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/ExecEndpointBuilderFactory.java b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/ExecEndpointBuilderFactory.java index b87fdca5846a..541e42bf451d 100644 --- a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/ExecEndpointBuilderFactory.java +++ b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/ExecEndpointBuilderFactory.java @@ -266,11 +266,13 @@ public interface ExecEndpointBuilderFactory { return this; } /** - * Whether to allow to use Camel headers or not (default false). - * Enabling this allows to specify dynamic command line arguments via - * message header. However this can be seen as a potential security - * vulnerability if the header is coming from a malicious user, so use - * this with care. + * Whether {code CamelExec} in-headers may override URI options (default + * false since Camel 4.20). When false, CamelExecCommandExecutable, + * CamelExecCommandArgs, CamelExecCommandOutFile, + * CamelExecCommandWorkingDir, CamelExecCommandTimeout, + * CamelExecExitValues, CamelExecUseStderrOnEmptyStdout, and + * CamelExecCommandLogLevel are ignored. Enable only when those headers + * come from a trusted route, not from an untrusted consumer. * * The option is a: <code>boolean</code> type. * @@ -285,11 +287,13 @@ public interface ExecEndpointBuilderFactory { return this; } /** - * Whether to allow to use Camel headers or not (default false). - * Enabling this allows to specify dynamic command line arguments via - * message header. However this can be seen as a potential security - * vulnerability if the header is coming from a malicious user, so use - * this with care. + * Whether {code CamelExec} in-headers may override URI options (default + * false since Camel 4.20). When false, CamelExecCommandExecutable, + * CamelExecCommandArgs, CamelExecCommandOutFile, + * CamelExecCommandWorkingDir, CamelExecCommandTimeout, + * CamelExecExitValues, CamelExecUseStderrOnEmptyStdout, and + * CamelExecCommandLogLevel are ignored. Enable only when those headers + * come from a trusted route, not from an untrusted consumer. * * The option will be converted to a <code>boolean</code> type. * @@ -445,7 +449,8 @@ public interface ExecEndpointBuilderFactory { /** * The name of the system command that will be executed. Overrides - * executable in the URI. + * executable in the URI. Requires allowControlHeaders=true on the exec + * endpoint or component (default is false since Camel 4.20). * * The option is a: {@code String} type. * @@ -459,7 +464,8 @@ public interface ExecEndpointBuilderFactory { /** * Command-line argument(s) to pass to the executed process. The * argument(s) is/are used literally - no quoting is applied. Overrides - * any existing args in the URI. + * any existing args in the URI. Requires allowControlHeaders=true on + * the exec endpoint or component (default is false since Camel 4.20). * * The option is a: {@code java.util.List<String> or String} type. * @@ -473,6 +479,8 @@ public interface ExecEndpointBuilderFactory { /** * The name of a file, created by the executable, that should be * considered as its output. Overrides any existing outFile in the URI. + * Requires allowControlHeaders=true on the exec endpoint or component + * (default is false since Camel 4.20). * * The option is a: {@code String} type. * @@ -485,7 +493,8 @@ public interface ExecEndpointBuilderFactory { } /** * The directory in which the command should be executed. Overrides any - * existing workingDir in the URI. + * existing workingDir in the URI. Requires allowControlHeaders=true on + * the exec endpoint or component (default is false since Camel 4.20). * * The option is a: {@code String} type. * @@ -498,7 +507,9 @@ public interface ExecEndpointBuilderFactory { } /** * The timeout, in milliseconds, after which the executable should be - * terminated. Overrides any existing timeout in the URI. + * terminated. Overrides any existing timeout in the URI. Requires + * allowControlHeaders=true on the exec endpoint or component (default + * is false since Camel 4.20). * * The option is a: {@code long} type. * @@ -511,7 +522,9 @@ public interface ExecEndpointBuilderFactory { } /** * The exit values for successful execution of the process. Overrides - * any existing exitValues in the URI. + * any existing exitValues in the URI. Requires allowControlHeaders=true + * on the exec endpoint or component (default is false since Camel + * 4.20). * * The option is a: {@code String} type. * @@ -552,7 +565,8 @@ public interface ExecEndpointBuilderFactory { /** * Indicates that when stdout is empty, this component will populate the * Camel Message Body with stderr. This behavior is disabled (false) by - * default. + * default. Requires allowControlHeaders=true on the exec endpoint or + * component (default is false since Camel 4.20). * * The option is a: {@code boolean} type. * @@ -566,7 +580,9 @@ public interface ExecEndpointBuilderFactory { /** * Logging level to be used for commands during execution. The default * value is DEBUG. Possible values are TRACE, DEBUG, INFO, WARN, ERROR - * or OFF (Values of LoggingLevel enum). + * or OFF (Values of LoggingLevel enum). Requires + * allowControlHeaders=true on the exec endpoint or component (default + * is false since Camel 4.20). * * The option is a: {@code String} type. *
