Skip to content

contrib-metrics: stop expanding dispatch inputs into the shell script - #2339

Open
nishantbkl3345-ship-it wants to merge 2 commits into
flatcar:mainfrom
nishantbkl3345-ship-it:fix/metrics-workflow-dispatch-input-injection
Open

contrib-metrics: stop expanding dispatch inputs into the shell script#2339
nishantbkl3345-ship-it wants to merge 2 commits into
flatcar:mainfrom
nishantbkl3345-ship-it:fix/metrics-workflow-dispatch-input-injection

Conversation

@nishantbkl3345-ship-it

Copy link
Copy Markdown

Stop expanding workflow_dispatch inputs into the metrics workflow's shell script

The Set the start and end dates step of .github/workflows/issue-pr-contrib-metrics.yaml expanded ${{inputs.start_date}} and ${{inputs.end_date}} directly into its run: block. GitHub Actions substitutes those expressions into the script text before bash is invoked, so the input values became part of the script itself rather than data read by it. A command substitution in either input was therefore executed by the runner.

The same step then appended the values verbatim to $GITHUB_ENV. Because $GITHUB_ENV is line-oriented, a newline inside an input defined arbitrary additional environment variables for every subsequent step of the job — including the steps that receive the org-wide read token secrets.GH_ACTION_METRICS_ORG_READ and the third-party actions that run under it. START_DATE/END_DATE are also interpolated into ten further run: blocks, so an unvalidated value reaches those shells as code too.

This is template-injection (CWE-94 / CWE-78). It is reachable on the real, current main: the workflow is live and its scheduled runs file reports such as #2218. The dispatch inputs are supplied by whoever triggers the run, so this converts "can dispatch a workflow" into "can run arbitrary commands on the runner and read the job's secrets", and it turns a simple typo (a stray quote, an &) into a confusing mid-run failure.

Fix. Pass both inputs through the step env: block so they stay data, then accept only plain YYYY-MM-DD values. Both halves are needed: the env: passthrough alone still lets a newline smuggle variables into $GITHUB_ENV (demonstrated by the mutation test below), and validation cannot be done safely without the passthrough. An incomplete date range is now rejected rather than being silently replaced by the computed fallback dates.

The scheduled path is unchanged: with no inputs, both variables expand to empty and the date -d fallback runs exactly as before.

How to use

Review .github/workflows/issue-pr-contrib-metrics.yaml — the change is confined to the first step (+20/−3). The other two files are the regression test and the workflow that runs it.

To reproduce the original bug, check out the parent commit and run the test; to confirm the fix, run it on this branch:

python3 -m pip install pyyaml
python3 .github/scripts/test_metrics_date_inputs.py

The test reproduces what the runner does — it expands the step's ${{ ... }} expressions, then executes the resulting run: block with bash against a temporary $GITHUB_ENV — so it exercises the real workflow file rather than a copy. It needs GNU date for the two cases that cover the scheduled fallback and skips them gracefully elsewhere.

Testing done

1. The bug, before the fix (main, on a host with GNU date, matching ubuntu-latest):

$ python3 .github/scripts/test_metrics_date_inputs.py
test_end_date_is_validated_too ... FAIL
test_explicit_dates_are_used ... ok
test_input_cannot_execute_commands ... FAIL
test_input_cannot_inject_extra_environment_variables ... FAIL
test_partial_input_is_rejected ... FAIL
test_scheduled_run_falls_back_to_computed_dates ... ok

AssertionError: True is not false : start_date was evaluated as shell code by the workflow step
AssertionError: True is not false : end_date was evaluated as shell code by the workflow step
AssertionError: 'PWNED' unexpectedly found in {'START_DATE': '2026-01-01', 'PWNED': 'yes', 'END_DATE': '2026-01-31'}
    : start_date smuggled an extra variable into $GITHUB_ENV

Ran 6 tests in 0.476s
FAILED (failures=4)

$(touch …) in an input created the marker file, and PWNED=yes landed in $GITHUB_ENV where every later step would inherit it.

2. After the fix:

$ python3 .github/scripts/test_metrics_date_inputs.py
Ran 6 tests in 0.101s
OK

Also run on a host without GNU date to confirm the skips behave: Ran 6 tests ... OK (skipped=2).

3. Mutation test — confirming validation is load-bearing, not decorative. Removing only the format check while keeping the env: passthrough:

Ran 6 tests in 0.098s
FAILED (failures=4)
AssertionError: 'PWNED' unexpectedly found in {'START_DATE': '2026-01-01', 'PWNED': 'yes', 'END_DATE': '2026-01-31'}

4. Static analysis (zizmor 1.16.3, --no-online-audits, whole .github/workflows/):

finding severity before after
template-injection High 4 0
template-injection Low 20 20
unpinned-uses High 18 18

The four High findings were exactly the four inputs.* expansions this PR removes. The new workflow reports no findings of its own.

5. Other checks: both workflow files parse as YAML; shellcheck -s bash over every run: block reports nothing for the changed step.

Pre-existing, unrelated findings (not touched)

  • unpinned-uses × 18 — the metrics actions are referenced by tag (@v1, @v2). Present before this PR, unchanged by it, and a separate hardening decision. The one action this PR adds is pinned to a SHA.
  • template-injection Low × 20${{ env.START_DATE }} / ${{ env.END_DATE }} in the later run: blocks. zizmor only reports these under the pedantic auditor persona. This PR makes them provably safe (the values can now only match [0-9]{4}-[0-9]{2}-[0-9]{2}), and rewriting ten unrelated blocks would have obscured the fix.
  • excessive-permissions — workflow-level issues: write. Pre-existing and out of scope.
  • shellcheck SC2129 (style) in the untouched Assemble full report step. Verified pre-existing by running shellcheck against the file at main.

Notes

The "Set the start and end dates" step of the monthly contributor report
expanded ${{inputs.start_date}} and ${{inputs.end_date}} directly into
its run block. Actions substitutes those expressions into the script
before bash sees it, so the values were parsed as shell code rather than
read as data, and a command substitution in either input was executed by
the runner.

The step also appended the values verbatim to $GITHUB_ENV. A newline in
an input therefore defined arbitrary extra variables for every later
step of the job, including the steps that receive the org-wide read
token in GH_ACTION_METRICS_ORG_READ.

Pass both inputs through the environment so they stay data, and accept
only plain YYYY-MM-DD values. Validation is needed on top of the
environment passthrough because the values are still written to
$GITHUB_ENV and interpolated into the search queries and report text of
the following steps. An incomplete date range is now rejected instead of
being silently replaced by the computed fallback dates.

Add a regression test that expands and runs the step the way the runner
does, plus a workflow to run it, since this report is otherwise only
exercised once a month.

Signed-off-by: Nishant <nishantbkl3345-ship-it@users.noreply.github.com>
@nishantbkl3345-ship-it
nishantbkl3345-ship-it requested a review from a team as a code owner August 16, 2026 11:27
Copilot AI lite review requested due to automatic review settings August 16, 2026 11:27

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

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

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

2 participants