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
{{ message }}
Repository navigation
security(ci): a conditional step can assemble its command words and bypass the scan guard through positions the walk cannot enumerate #3582
A conditional run: step can assemble its command words from fragments so that no scan text appears anywhere in the scalar, then execute the reduced scan through some syntactic position the guard's command-position walk does not reach:
"$K" "$W" "$S" "$F" nsa — no plain scan word and no literal --framework, so allExpandedBeforeFramework never runs
All five were found by review on #3546 and all five are real: bash executes a reduced scan that can overwrite the SARIF while a separate nsa,mitre invocation satisfies the validator.
Why this is filed rather than fixed
Patching positions does not converge.#3546 closed the spelling class by inverting the conditional screen from "recognise a scan" to "refuse unless shown scan-free". That worked. But once the attacker assembles command words from fragments, the remaining question is "which syntactic positions can hold a command", and enumerating those is the same losing game one level down — four of the five above are simply positions the previous round's walk did not visit, and there is no argument that a fifth round would be the last.
The obvious strong rule is not viable, and that is measured rather than assumed. Refusing any conditional step that contains any expansion closes the whole family at a stroke. It also refuses the real ci.yaml: its SARIF-normalising step is conditional and legitimately uses ${RUNNER_TEMP} and ${GITHUB_OUTPUT}. Implemented and run, it failed all three real-workflow tests. So the cheap terminal rule would reject the very workflow this guard exists to validate.
This is not a regression. Every one of these five is bypassable on main today — #3546 does not introduce them, it closes thirteen other bypasses while leaving these. Holding that work to avoid these would keep strictly more holes open.
Direction
The decidable question is not "does this text invoke the scan" but "is this step's shape simple enough to read at all". A conditional step could be required to match a small allow-list of provably-simple shapes — a literal command word, literal arguments, no substitution, no eval, no reserved-word compound — and refused otherwise, with the refusal naming the shape. That inverts at the level where the current design still enumerates, and its DevEx cost needs measuring against the real workflows before it is adopted.
Acceptance criteria:
All five reproductions above refuse.
The three real-workflow tests still pass, and the conditional steps in ci.yaml that legitimately use expansions still pass.
One shared fixture table drives the conditional and unconditional paths, so a position added for one is asserted against the other.
Whatever rule is chosen ships with a control proving it did not collapse into "refuse every conditional step", which every refusal test would otherwise pass.
Delivered by merged PR #4431, reviewed source commit 06d951f.
The conditional and unconditional paths now consume one bounded AST analysis. The shared regression table refuses all five original assembled-command cases across both paths. Controls accept ordinary conditional steps, the legitimate environment expansions in the real workflows, and the three original real-workflow tests.
Acceptance was checked against the original issue body. Current-head race tests passed all 266 test/subtest assertions, including 28 Bash recording witnesses; targeted safeguard mutations failed the intended regressions. The original historical test file is unchanged, and source CI plus protected merge-queue CI passed. Closing as completed.
Activity
chore: agent-claim #3582 nonce=361326e001927c0e86cefee2c081e307
chore: agent-claim #3582 nonce=4dc8353e75bea14eae290fc9713f0c6e
devantler commentedon Oct 3, 2026
Delivered by merged PR #4431, reviewed source commit 06d951f.
The conditional and unconditional paths now consume one bounded AST analysis. The shared regression table refuses all five original assembled-command cases across both paths. Controls accept ordinary conditional steps, the legitimate environment expansions in the real workflows, and the three original real-workflow tests.
Acceptance was checked against the original issue body. Current-head race tests passed all 266 test/subtest assertions, including 28 Bash recording witnesses; targeted safeguard mutations failed the intended regressions. The original historical test file is unchanged, and source CI plus protected merge-queue CI passed. Closing as completed.