Problem
RunShellExecutor passes the assembled command string to ProcessBuilder("sh", "-c", command). This means shell metacharacters (;, |, &&, $(...), backticks) in jq-resolved values are interpreted by the shell, making any command allowlist trivially bypassable.
For example, even if only echo is allowed:
command: ${ "echo " + .user.input }
An input of hello; rm -rf / becomes sh -c 'echo hello; rm -rf /' — the shell executes both commands.
Proposal
1. Replace sh -c with direct ProcessBuilder execution using an argv array.
The spec already separates command (the executable) and arguments (the parameters). Map them directly:
command → argv[0]
arguments entries → argv[1..n] (key-value pairs become "--key=value", key-only becomes "--key")
command: echo
arguments:
--user: john
Becomes: ProcessBuilder("echo", "--user=john") — no shell involved.
Without a shell, metacharacters are literal strings, not operators. Injection is eliminated structurally, not by filtering.
2. Add a command allowlist to WorkflowApplication.
WorkflowApplication.builder()
.allowedShellCommands(List.of("echo", "grep", "curl"))
.build()
- The resolved
command (argv[0]) is compared against the allowlist as an exact string match.
- Empty list (default) = no shell execution allowed.
- Integrating frameworks pass their configuration through this API.
3. Pipes, subshells, and redirection are no longer available.
This is intentional. Users who need pipelines should wrap them in a dedicated script and allowlist that script name. This keeps the trust model clear: deployers control what executables are available, workflow authors control how to invoke them.
Scope
RunShellExecutor and RunShellExecutorBuilder in impl-core
WorkflowApplication (new allowlist API)
CommandPropertySetter in impl-container is a separate concern (tracked separately)
Problem
RunShellExecutorpasses the assembled command string toProcessBuilder("sh", "-c", command). This means shell metacharacters (;,|,&&,$(...), backticks) in jq-resolved values are interpreted by the shell, making any command allowlist trivially bypassable.For example, even if only
echois allowed:An input of
hello; rm -rf /becomessh -c 'echo hello; rm -rf /'— the shell executes both commands.Proposal
1. Replace
sh -cwith directProcessBuilderexecution using an argv array.The spec already separates
command(the executable) andarguments(the parameters). Map them directly:command→argv[0]argumentsentries →argv[1..n](key-value pairs become"--key=value", key-only becomes"--key")Becomes:
ProcessBuilder("echo", "--user=john")— no shell involved.Without a shell, metacharacters are literal strings, not operators. Injection is eliminated structurally, not by filtering.
2. Add a command allowlist to
WorkflowApplication.command(argv[0]) is compared against the allowlist as an exact string match.3. Pipes, subshells, and redirection are no longer available.
This is intentional. Users who need pipelines should wrap them in a dedicated script and allowlist that script name. This keeps the trust model clear: deployers control what executables are available, workflow authors control how to invoke them.
Scope
RunShellExecutorandRunShellExecutorBuilderinimpl-coreWorkflowApplication(new allowlist API)CommandPropertySetterinimpl-containeris a separate concern (tracked separately)