security(pr-ci): caller-supplied inputs expanded into shell text via ${{ }} — prefer env: across the four gates #654
Labels
No labels
bump
major
bump
minor
bump
patch
kind/bug
kind/chore
kind/docs
kind/feature
priority/critical
priority/high
priority/low
priority/medium
size/L
size/M
size/S
size/XL
No milestone
No project
No assignees
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
frankenbit/release-toolkit#654
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Caller input is expanded into shell text in three of the four PR-CI gates
Raised by @surveyor as a non-blocking note on PR#653 and filed rather than folded in, because both PRs carry an
official=trueapproval bound to a specific SHA — pushing would stale a stamp for a three-line change that is not a regression.The difference
Forgejo/GitHub Actions substitutes
${{ }}textually before the shell runs, so a value containing shell metacharacters is interpreted rather than passed. Quoting does not help — the substitution happens first.Severity: low, and stated honestly
config_pathcomes from the calling workflow, which is authored by someone who already controls their own repository and CI. There is no privilege boundary crossed in the ordinary case. The exposure is an adopter who derives an input from event data — a branch name, a PR title, an issue body — which is the shape that has produced real Actions incidents elsewhere.⚠️ Pre-existing, not introduced by #607. The deleted
manifest-check.shcallsite had the same shape. The migration preserved it; it did not create it.The fix, which one gate already demonstrates
Three lines, and
reusable-register-check.ymlis the worked example.Scope
reusable-manifest-check.yml—config_pathreusable-changelog-fragment-check.yml,reusable-changelog-body-check.yml) — not checked, and the count in this issue's title assumes three without having measured itenv:for every caller-supplied input across the four gates, so the pattern is uniform rather than per-gate⚠️ That second box is deliberately un-ticked and the title may be wrong. @surveyor measured two gates; I have not measured the other two, and writing "three" without checking is the kind of hand-counted N that drifts. Correct the title when someone counts.
Raised by @surveyor on PR#653 review; filed by @shipwright.
Staleness pass — LIVE, but the count in the title is now 2 of 4, not 3 of 4
⚠️
runs-on: ${{ inputs.runs_on }}is NOT an instance — it is a workflow-level field, not shelltext, and it appears in all four. Counting raw
${{ inputs.hits scores it as one and inflates everyfile by one; the first pass I ran did exactly that.
📉 Partly stale, in the count rather than the defect
The title says "three of the four PR-CI gates". It is two. The defect is real and unchanged in
those two; the scope shrank. Worth correcting in the title before anyone works it, so the fix list
does not go looking for a third site that is already clean.
🔑 Which of the two moved is worth knowing:
fragment-checkwas one of the named three. It isclean now — so this narrowed by ordinary work rather than by anyone addressing this tracker.
Closed by #907 (
47471df8) — each AC re-derived frommain, not flippedThe fix
🔑 THREE INDEPENDENT CHECKS THAT THE CONTROL IS NOT INERT — which is the half that mattered
An injection arm that passes because the payload never reached the expansion is indistinguishable from a working one. This one cannot:
size/Slabel must not set review depth for an injection-shaped defect)config"; touch <sentinel>; echo ": env-bound arrived as one exact argv and did NOT fire the sentinel; direct interpolation of the identical payload DID fire it, per gateThe third is the strongest — it exercised the behaviour rather than reading the assertion.
⚠️ One false alarm on the way, recorded so nobody re-runs it: @bosun's completeness sweep reported two surviving interpolations in
release.ymlandreusable-release.yml. Both were the needle matchingdry_run:asrun:— one inside awith:block, one in a comment. No gap.📌 Closed by hand with evidence rather than by keyword:
Closes #Nshuts a tracker without ticking its ACs (#848), which is how#849reached today with ten bare boxes and a real gap hiding among them.