fix(gate): require a Bot author type alongside the release-please branch shape - #42
Conversation
…nch shape `github.head_ref` is the branch name chosen by whoever opens the PR, so a branch-shaped waiver is a predicate the checked party controls. Anyone able to open a PR could name a branch `release-please--branches--x` and take the waiver on any content. Four sites, and the two files differ in what they were waiving: - hybrid-gate.yml's `Verify no AI attribution` exited 0 on the branch pattern alone, one line above a dependabot waiver that does check the author. The asymmetry was the tell. This workflow is called by 11 repos. - gate-attestation.yml waived the Gate-Passed trailer verification itself, and its negated forms skipped both the checkout and the verify step. The branch pattern stays, because it is the half that is STABLE: release-please's login varies (github-actions[bot] under GITHUB_TOKEN, a PAT owner after kanon#1092) while its branch shape does not. The error was treating stable and unforgeable as the same property. `user.type` is both — GitHub derives it from the resolved account and a pusher cannot set it. Neither condition alone is now sufficient. gate-attestation.yml already carried a WHY calling head_ref attacker-controlled text, as the reason for env-var indirection against shell injection. The same property disqualifies it as a trust predicate; only one of the two conclusions had been drawn. No effect on current releases: release-please authors as github-actions[bot], type Bot, so the waiver still fires. When kanon#1092 moves it to a PAT-owned account the waiver stops firing and that PR goes red rather than silently exempt — the correct direction to fail, and a one-line allowlist addition then.
Pre-fix capture — vulnerability confirmed live by executionProbe: forkwright/aletheia#6858, pinned at the pre-fix SHA of Shape: branch Result — both waivers fired.
Nine seconds is not a scan. The job log carries the decisive line: So this is observed, not argued: an arbitrary author naming a branch The attacker population is not emptyWhile this probe was running, forkwright/aletheia#6854 arrived from an external account ( That PR is unrelated slop and is being closed on its merits. It matters here only for one reason: it removes the last reason to treat this waiver as theoretical. The capability to open a PR — the only capability the bypass requires — is demonstrably held by parties outside the org. Post-fix captureFollows once this merges and aletheia's pin is bumped. Same branch, same commit, same author; only the pinned workflow changes. Expected: both checks run, and both fail. |
Post-fix half of the forkwright/.github#42 verification. Same branch, same author, same probe content; only the pinned workflow changes. Refs forkwright/.github#42
Post-fix capture — the fix is proven by executionProbe aletheia#6858, pin bumped to
The decisive line from the post-fix run: That is the waiver declining to fire for a human author on a release-please-shaped branch, which is exactly the defect. The third row confirms the check then does its job. One honest limit on this capture. The middle row taught me my own probe was initially mis-aimed: I put the marker in the PR body, and this check reads commits. A pass proved nothing until the marker moved into the commit. Worth recording because the mis-aimed version looked like a successful verification.
Probe closed and branch deleted. |
Bumps the shared-gate pin to `08a0bad`, which carries the release-please waiver fix (forkwright/.github#42). ## Why this matters here The prior pin (`54f1af7`) waives the AI-attribution check on `github.head_ref` alone — a branch name chosen by whoever opens the PR, with no author condition. That was **confirmed live on this repo**, not argued: | capture | pin | marker in | result | |---|---|---|---| | pre-fix | `54f1af7` | commit | **pass in 9s** — waiver fired, `exit 0`, nothing scanned | | post-fix | `08a0bad` | commit | **fail** — waiver refused, check ran, caught it | Probe #6858 (now closed, branch deleted). The decisive post-fix line: ``` Branch release-please--branches--waiver-probe has the release-please shape but author forkwright is type User, not Bot — not waiving. ``` aletheia is public, and an external account opened #6854 on this repo while that probe was running — so the capability the bypass requires is demonstrably held outside the org. ## No effect on releases All three open release-please PRs fleet-wide (typikon#174, kanon#3306, logismos#109) are authored by `github-actions[bot]`, `user.type = Bot`. They still waive. Verified rather than assumed, because tightening a waiver is exactly the change that silently reddens a release flow. ## Wider note, not fixed here Three consumer repos pin this workflow at **three different SHAs**, all pre-fix. A security fix in the shared gate reaches an adopting repo only when that repo bumps, and nothing reports the lag. That is a propagation gap worth its own mechanism; flagging rather than solving it in a pin bump. Co-authored-by: Cody Kickertz <admin@forkwright>
The hole
github.head_refis the branch name chosen by whoever opens the PR. Both gate workflows used it as a trust predicate, sufficient on its own:Anyone able to open a PR names their branch
release-please--branches--xand takes the waiver on any content. The asymmetry with the dependabot waiver one line below is the tell.Four sites, and the two files waive different things:
hybrid-gate.yml:465gate-attestation.yml:31gate-attestation.yml:53,60(negated)The second file is the sharper one: it is not policy evasion, it is skipping the attestation that a PR's tip was actually gated — in the repo that hosts the reusable workflows the other eleven execute.
The fix
Both conditions necessary, neither sufficient. The branch pattern stays, because it is the half that is stable: release-please's login varies (
github-actions[bot]underGITHUB_TOKEN, a PAT owner after kanon#1092) while its branch shape does not.The error was treating stable and unforgeable as the same property.
user.typeis both — GitHub derives it from the resolved account and a pusher cannot set it.The part worth noticing
gate-attestation.ymlalready carried this, as justification for env-var indirection:The author knew the property and drew one of its two conclusions: defended against shell injection with it, then used it as the trust predicate eleven lines above. Same fact, one consequence applied.
No effect on current releases
release-please authors as
github-actions[bot], typeBot, so the waiver still fires today. When kanon#1092 moves it to a PAT-owned (human) account,user.typebecomesUser, the waiver stops firing, and that release PR goes red rather than silently exempt — the correct direction to fail, and a one-line allowlist addition at that point.Verification status — read this before merging
This is unverified by execution, and I am not claiming otherwise.
VERIFICATION.mdrequires observed pre-fix failure and observed post-fix success; what I have is a source-level argument plus valid YAML.Both gate workflows are
workflow_call-only, and this repo runs onlyactionlinton its own PRs — so a probe PR here never executes them and would prove nothing. The negative case has to be observed in a consuming repo, against a pinned SHA.That is being done in aletheia: a probe PR from a
release-please--branches--…branch carrying an attribution marker, observed before the pin bump (expect: waived, vulnerability live) and after (expect: caught). I will post both captures here before this merges.Related: gnomon#51's
Done whenasks for exactly that — "proven by a check that goes red on that shape". This repo has no test harness at all (it isREADME.mdplus.github/workflows/), so there is nowhere for a negative-case fixture to live permanently. Standing one up is a separate design call, and it is the difference between this fix being argued and being guarded.Credit: found by a completion audit in a peer seat and handed over rather than patched, since this repo is mine.