Skip to content

fix(gate): require a Bot author type alongside the release-please branch shape - #42

Merged
forkwright merged 1 commit into
mainfrom
fix/waiver-requires-bot-author
Aug 17, 2026
Merged

fix(gate): require a Bot author type alongside the release-please branch shape#42
forkwright merged 1 commit into
mainfrom
fix/waiver-requires-bot-author

Conversation

@forkwright

Copy link
Copy Markdown
Owner

The hole

github.head_ref is the branch name chosen by whoever opens the PR. Both gate workflows used it as a trust predicate, sufficient on its own:

# hybrid-gate.yml — Verify no AI attribution
case "$PR_HEAD_REF" in
  release-please--branches--*)
    exit 0          # <- no author check
    ;;
esac
if [ "$PR_AUTHOR" = "dependabot[bot]" ]; then   # <- the very next waiver DOES check the author

Anyone able to open a PR names their branch release-please--branches--x and 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:

file what the branch predicate alone was skipping reach
hybrid-gate.yml:465 the AI-attribution attestation 11 repos call this
gate-attestation.yml:31 the entire "trusted automation" pass this repo
gate-attestation.yml:53,60 (negated) the checkout and the Gate-Passed trailer verification this repo

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] 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.

The part worth noticing

gate-attestation.yml already carried this, as justification for env-var indirection:

head_ref is attacker-controlled text — env-var indirection keeps it out of shell interpolation

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], type Bot, so the waiver still fires today. When kanon#1092 moves it to a PAT-owned (human) account, user.type becomes User, 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.md requires 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 only actionlint on 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 when asks for exactly that — "proven by a check that goes red on that shape". This repo has no test harness at all (it is README.md plus .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.

…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.
@forkwright

Copy link
Copy Markdown
Owner Author

Pre-fix capture — vulnerability confirmed live by execution

Probe: forkwright/aletheia#6858, pinned at the pre-fix SHA of hybrid-gate.yml.

Shape: branch release-please--branches--waiver-probe, human author, no Gate-Passed trailer on the tip, attribution marker in the body.

Result — both waivers fired.

check result duration
hybrid-gate / ai-attribution pass 9s
hybrid-gate / check-trailer pass 9s

Nine seconds is not a scan. The job log carries the decisive line:

release-please--branches--*)
  echo "Release-please branch ($PR_HEAD_REF): AI attribution check exempt."
  exit 0
  ;;

So this is observed, not argued: an arbitrary author naming a branch release-please--branches--… skips the AI-attribution check and the Gate-Passed trailer verification, on any content.

The attacker population is not empty

While this probe was running, forkwright/aletheia#6854 arrived from an external account (alienvisitor8675-bit) on a bounty-fix-… branch. aletheia is public; outside parties open PRs on it today.

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 capture

Follows 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.

@forkwright
forkwright merged commit 08a0bad into main Aug 17, 2026
1 check passed
@forkwright
forkwright deleted the fix/waiver-requires-bot-author branch August 17, 2026 15:32
forkwright pushed a commit to forkwright/aletheia that referenced this pull request Aug 17, 2026
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
@forkwright

Copy link
Copy Markdown
Owner Author

Post-fix capture — the fix is proven by execution

Probe aletheia#6858, pin bumped to 08a0bad (this PR's merge commit). Same branch, same author, same repo; only the pinned workflow changed.

capture pin marker location ai-attribution
pre-fix 54f1af7 PR body pass — waiver fired, exit 0, nothing scanned
post-fix 08a0bad PR body pass — waiver refused, check ran, found nothing (marker was in the wrong place for what this check reads)
post-fix 08a0bad commit trailer fail — waiver refused, check ran, caught it

The decisive line from the post-fix run:

Branch release-please--branches--waiver-probe has the release-please shape
but author forkwright is type User, not Bot — not waiving.

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.

check-trailer is NOT verified by this probe. It passed in every run — but via docs_only_exemption, an independent and legitimate waiver that my probe's file types (.txt, .yml) trigger. Testing the trailer waiver needs a probe touching non-docs paths. The gate-attestation.yml sites are fixed by the same reasoning and reviewed, but they are argued, not observed, and I am not going to call them verified.

Probe closed and branch deleted.

forkwright added a commit to forkwright/aletheia that referenced this pull request Aug 17, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant