You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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:124 — git 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.
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.
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.
Why (verified evidence)
83 distinct
${{ … }}expressions across 34 workflow files are expanded directly intorun:orscript:block scalars. GitHub's documented guidance is to pass such values throughenv: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:124—git 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 3× inside onerun:block (if [ "${{ inputs.repos }}" != "all" ] …; then repos="${{ inputs.repos }}")..github/workflows/maint-72-fix-pr-body-conflicts.yml—${{ inputs.target_repo }}intorun:..github/workflows/reusable-codex-run.yml— 6 expressions intorun:, including${{ inputs.codex_args }}, which is free-form argument text..github/workflows/agents-72-codex-belt-worker.yml— 8 expressions intoscript: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:
inputs.*fromworkflow_dispatch, which only an actor with write access can set. The privilege gained is therefore roughly the privilege already held.github.event.issue.title,.body,pull_request.title,.body,comment.body, orhead.refinterpolated into a script body.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
maint-70-fix-integration-formatting.yml:124(inputs.commit_message) andreusable-codex-run.yml(inputs.codex_args) — move each to a step-levelenv:entry and reference"$VAR"in the script.maint-69-sync-labels.yml's 3×${{ inputs.repos }}in the "Determine target repos" step the same way..github/workflows/*.ymlinventory: convertrun:/script:interpolations toenv:indirection where free-form; document deliberate exceptions inline where provably constrained (e.g. boolean or number)..github/workflows/and fails on${{ inputs.* }}/${{ github.event.* }}inside arun:/script:block scalar, with an explicit allowlist for reviewed exceptions.Acceptance Criteria
tests/workflows/test_no_untrusted_interpolation.py::test_no_untrusted_expressions_in_script_bodies— parses each workflow, walksrun:/script:block scalars, and asserts noinputs./github.event.expression appears except those in an explicit, commented allowlist.git commit -m "${{ inputs.commit_message }}"inmaint-70-fix-integration-formatting.yml→ confirm the named test FAILS → revert.maint-70withcommit_messageset toa"band confirm the step succeeds (today it produces a brokengit commitcommand). 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
Surfaced while diagnosing #3009. Inventory produced by scanning all 131 workflow files on
mainvia the Contents API and walkingrun:/script:block scalars; exemplars re-read individually at the cited lines.