Skip to content

[P2] 83 untrusted ${{ }} expressions interpolated into run:/script: bodies across 34 workflows — rework to env: indirection #3016

Description

@stranske

Why (verified evidence)

83 distinct ${{ … }} expressions across 34 workflow files are expanded directly into run: or script: block scalars. GitHub's documented guidance is to pass such values through env: and reference them as shell/JS variables, because direct expansion splices the value into the script text before the shell ever sees it. Two consequences: a value containing shell metacharacters executes, and a value containing a space or quote simply breaks the step.

Verified exemplars (read on main):

  • .github/workflows/maint-70-fix-integration-formatting.yml:124git commit -m "${{ inputs.commit_message }}". A free-text input spliced inside double quotes. A message containing " terminates the string; $(…) or backticks execute. This is the sharpest instance: the input is free text by design.
  • .github/workflows/maint-69-sync-labels.yml${{ inputs.repos }} appears inside one run: block (if [ "${{ inputs.repos }}" != "all" ] …; then repos="${{ inputs.repos }}").
  • .github/workflows/maint-72-fix-pr-body-conflicts.yml${{ inputs.target_repo }} into run:.
  • .github/workflows/reusable-codex-run.yml — 6 expressions into run:, including ${{ inputs.codex_args }}, which is free-form argument text.
  • .github/workflows/agents-72-codex-belt-worker.yml — 8 expressions into script: bodies (inputs.issue, inputs.branch, inputs.base, inputs.source, …).

Full inventory: 83 (file, expression) pairs in 34 files. The complete list is reproducible with the scan described under "Test gate".

Honest severity assessment

This is not a critical externally-exploitable vulnerability, and the issue should not be worked as though it were:

  • The overwhelming majority are inputs.* from workflow_dispatch, which only an actor with write access can set. The privilege gained is therefore roughly the privilege already held.
  • The scan found no instances of the genuinely dangerous fields — no github.event.issue.title, .body, pull_request.title, .body, comment.body, or head.ref interpolated into a script body.
  • The handful of github.event.* hits are numeric or repo-controlled (issue.number, repository.default_branch, pull_request.base.ref).

What it is: a systemic robustness and hygiene defect that also breaks on ordinary input (any commit message with a quote), and a pattern that will eventually meet an untrusted field as these workflows grow. It is worth a deliberate pass, not an emergency.

Tasks

  • Fix the free-text exemplars first, since these break on ordinary input: maint-70-fix-integration-formatting.yml:124 (inputs.commit_message) and reusable-codex-run.yml (inputs.codex_args) — move each to a step-level env: entry and reference "$VAR" in the script.
  • Fix maint-69-sync-labels.yml's 3× ${{ inputs.repos }} in the "Determine target repos" step the same way.
  • Triage remaining .github/workflows/*.yml inventory: convert run:/script: interpolations to env: indirection where free-form; document deliberate exceptions inline where provably constrained (e.g. boolean or number).
  • Add a repository guard so the pattern cannot silently return — a test that parses every file in .github/workflows/ and fails on ${{ inputs.* }} / ${{ github.event.* }} inside a run:/script: block scalar, with an explicit allowlist for reviewed exceptions.

Acceptance Criteria

  • Named test: tests/workflows/test_no_untrusted_interpolation.py::test_no_untrusted_expressions_in_script_bodies — parses each workflow, walks run:/script: block scalars, and asserts no inputs./github.event. expression appears except those in an explicit, commented allowlist.
  • Deliberate-break → revert: re-introduce git commit -m "${{ inputs.commit_message }}" in maint-70-fix-integration-formatting.yml → confirm the named test FAILS → revert.
  • Behavioural check for the exemplar: dispatch maint-70 with commit_message set to a"b and confirm the step succeeds (today it produces a broken git commit command). Note this dispatch is currently blocked by [P1][ops] All agent issue-automation workflows halt at action_required with zero jobs — agents:auto-pilot is a no-op #3009.

Non-Goals

  • Do NOT treat this as a critical security incident or rewrite unrelated workflow logic; the scope is expression handling only.
  • Do NOT change what any workflow does — behaviour must be identical for well-formed inputs.
  • Do NOT bulk-convert every hit blindly; provably constrained values (booleans, numbers) may stay, but must be allowlisted deliberately rather than skipped silently.
  • No scaffolding / TODO-only changes; every task is a concrete edit verified by the gate above.

Surfaced while diagnosing #3009. Inventory produced by scanning all 131 workflow files on main via the Contents API and walking run:/script: block scalars; exemplars re-read individually at the cited lines.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    priority:normalNormal-priority weekly repo-review work

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions