fix(ci): eliminate script injection in pending-decision.yml#1395
Conversation
The reusable workflow interpolated ${{ inputs.pr_number }} and
${{ inputs.decision }} directly into the github-script JS source:
const prNumber = ${{ inputs.pr_number }};
const decision = '${{ inputs.decision }}';
inputs.decision is a free-form string (only documented by a comment as
"maintainer" or "contributor", not enforced by a choice type or any
validation). A crafted decision value could break out of the single-quoted
JS string literal and execute arbitrary code inside the github-script
sandbox, which holds pull-requests:write + issues:write permissions.
inputs.pr_number is typed as `number`, which GitHub Actions coerces at
the workflow-call boundary, but relying on the caller's input schema
alone is fragile defense — the value is still substituted as raw text
before the JS parser ever sees it.
This workflow currently has no caller in the repo (workflow_call with no
invoking workflow found), so there's no live exploit path today. Fixing
it now since it's a footgun for whoever wires it up next.
Fix: pass both inputs through env: (the pattern used elsewhere in this
repo's workflows, and the one GitHub's own security hardening guide
recommends), and read them via process.env inside the script instead of
string-interpolating into the JS source. Number() replaces the implicit
number coercion; process.env values are never eval'd as code regardless
of content, closing the injection regardless of the input's declared
type.
|
Note LGTM ✅ — Correct, minimal, idiomatic script-injection fix. No blocking findings. What This PR Does
How It Worksenv:
PR_NUMBER: ${{ inputs.pr_number }}
DECISION: ${{ inputs.decision }}const prNumber = Number(process.env.PR_NUMBER);
const decision = process.env.DECISION;
Findings
Finding Details🟢 F1: Canonical fix patternThe 🟢 F2: Clear blast-radius documentationThe PR body accurately identifies that 🟢 F3: Zero behavior driftOnly the value-delivery mechanism changed. The label-flip logic, ternary conditions, API calls, and console output are untouched. Baseline Check
5️⃣ Three Reasons We Might Not Need This PR
|
What
Fixes a script-injection vulnerability in
pending-decision.yml, found during a follow-up audit after #1393.The
github-scriptstep interpolated bothworkflow_callinputs directly into the JS source string:inputs.decisionistype: stringwith nochoiceconstraint or runtime validation — only a comment documents the expected values ("maintainer"/"contributor"). A crafteddecisionvalue could break out of the single-quoted JS literal and execute arbitrary code inside thegithub-scriptsandbox, which runs withpull-requests: write+issues: write.Blast radius
This
workflow_callreusable workflow currently has no caller in the repo — searched.github/workflows/for anyuses:reference and found none.pending-maintainer.yml/issue-pending-maintainer.ymlboth implement the label-flip logic inline rather than calling this workflow. So there's no live exploit path today, but the footgun is real the moment something calls it with attacker-influenced input (e.g. a bot command parsed from a PR comment).Fix
Same pattern as the GitHub Actions security hardening guide (and the one already applied in #1393): pass values through
env:and read them viaprocess.envinside the script, never string-interpolated into the JS source.PR_NUMBER→Number(process.env.PR_NUMBER)DECISION→process.env.DECISIONprocess.envvalues are never parsed as code, so this closes the injection regardless of the input's declared type — not just for the currentstring/numbertypes but for any future schema change too.No behavior change: same label-flip logic, just a safe value-delivery mechanism.
Verification
actionlintclean (exit=0)