You gate privileged fork-PR runs behind maintainer approval or a safe-to-test label. How can a contributor still get newer code run?
answer
- the gate names a moving object
- check time versus checkout time
- labels outlive the commit they blessed
- bind approval to an immutable digest
- clear the blessing on every push
basics
~20 sBy pushing after the human looks. Approval and labels attach to the pull request, not to a commit, so the job resolves the branch head when it starts, and a gate that survives later pushes blesses code nobody reviewed.
solid answer
~50 sBecause the gate names a pull request while the job runs a commit, and the two can differ. There are two variants. The race: a maintainer reads the diff and releases the run, and the contributor pushes a new head into the window before the runner materialises the job and resolves the branch — the checkout takes whatever is current, not what was read. The persistence: the label or approval stays attached across subsequent pushes, so every later commit inherits a blessing granted to an earlier one. The fix is to bind the decision to content: record the reviewed commit digest at approval time, take that digest from the platform's own record rather than from the event payload, and check out exactly it; clear the label and dismiss the approval on every new push. And treat all of that as defence in depth — a human gate in front of a credentialed job is still a credentialed job running someone else's code.
go deeper
Remember that approving a pull request is not the same as approving a commit, and that a contributor can push again after a maintainer has looked.
Explain the mechanics: the job resolves the branch head at start time, and the label or approval persists across pushes, so a later commit inherits an earlier blessing.
Show the concrete fix — capture the reviewed digest from a trusted source, check out exactly it, expire the blessing on every push — and say plainly that this is defence in depth, not the cure.
Argue for where human gates belong at all: they buy attention, not authorisation, and an organisation that relies on them is asking reviewers to certify executable safety at a rate no reviewer sustains.
## Why teams reach for a gate at all A maintainer wants contributors to get real feedback, and some of that feedback needs credentials. Rather than redesigning the pipeline, the common move is to put a human in front of it: the platform holds runs from first-time or outside contributors until someone releases them, or the team adopts a convention where a maintainer applies a label such as `safe to test` after reading the diff. The instinct is sound — a person looked. The implementation has a gap that is easy to state and easy to exploit. ## The gap: the gate names a pull request, the job runs a commit A pull request is a *moving* object. It identifies a source branch, and that branch's head changes whenever the contributor pushes — including a force-push that rewrites history so the old commit is no longer even reachable. The human decision, though, is recorded against the pull request, not against the bytes that were read. That produces two distinct failures, and a strong answer names both: **1. The window (a time-of-check to time-of-use race).** Between the maintainer releasing the run and the runner actually starting the job and resolving the branch, there is a real interval — queueing, capacity, image pull. A contributor who pushes in that window has their newest commit checked out by a job the maintainer approved on the strength of different content. Nothing anomalous happens from the platform's point of view: the job asked for the pull request's head, and it got the pull request's head. **2. The persistence.** A label sits on the pull request until someone removes it, and an approval usually blesses more than the single commit that prompted it. So a contributor can open a benign pull request, collect the label, and push the interesting commit afterwards — no timing skill required. This is the variant that a patient attacker with a throwaway account will use, because it does not depend on winning a race. The scenario worth picturing: a widely-used scientific library whose maintainers release runs for new contributors, and whose credentialed job holds the package-index publish token. A first contribution of a docstring fix collects the release; the follow-up push carries the payload; the token is gone. There was never a moment when a maintainer failed to do their job. ## Binding the decision to content The fix is to make the gate refer to something immutable — a commit digest — instead of to a moving reference: - **Record what was reviewed.** At approval time, capture the head commit digest and store it where the contributor cannot influence it, which means the platform's own record of the approval or a maintainer-authored comment, not a value read out of the event payload the contributor supplied. - **Check out exactly that digest**, never the branch name or the head ref. If the current head differs, the run should stop and demand re-approval — and that mismatch is worth alerting on, because it is a clean signal that someone probed the gate. - **Expire the blessing on every push.** Remove the label and dismiss the approval whenever the pull request's head changes, so that a stale blessing cannot be carried forward. - **Pin what the commit pulls in, too.** A frozen digest still leaves anything the build fetches at run time free to change. If the job resolves dependencies or fetches submodules from mutable references, the content the maintainer read is not the content that executes. ## Why the gate is still not the answer Even a perfectly commit-bound gate asks a person to certify that a diff is safe *to execute with credentials*. That is a much harder review than "is this change correct": it means auditing every executable input the checkout brings — build scripts, task configuration, test fixtures, dependency declarations — and doing so under time pressure, repeatedly, for strangers. Human review does not scale to that, and reviewers habituate. So the ranking of controls is: 1. **Do not run untrusted code in a credentialed job.** Split the work; let the untrusted half produce inert data. 2. If some privileged action truly must be tied to the contributor's content, **bind it to a reviewed digest** and expire the blessing on every push. 3. **Instrument the gap**: record both the approved digest and the executed digest on every run, and alert when they differ. ## Interview shape The answer an interviewer is listening for is the sentence "the approval names the pull request, but the checkout resolves the head", followed by the observation that persistence is the easier exploit than the race, followed by the digest-binding fix and the admission that it is a mitigation, not the cure. Candidates who stop at "we require approval" have described the control that was already in place when the incident happened.
- You now check out the reviewed digest. What else must you pin?Everything the build pulls in after checkout. Submodules, dependency ranges resolved at run time, tool installers and container images fetched by tag all sit outside the frozen commit, so content the maintainer never read can still execute. Freeze resolution with a lockfile committed at that digest, and fetch tools by digest, or the review covers only part of what runs.
- Why does 'only maintainers can merge' not help here?Because the run happens before merge. The whole point of pull-request CI is to execute the proposal, so merge permissions, required reviews and protected branches never come into play. The attacker is not trying to land code in your repository; they are trying to make your pipeline execute code once, with credentials present.
- What telemetry tells you someone probed the gate?Record two digests per privileged run: the one that was approved and the one that was executed. Any run where they differ is either a defeated gate or a race you got lucky on, and both deserve investigation. Also alert on a push arriving within seconds of an approval, which is the shape of a deliberate attempt rather than a coincidence.
Signing a blank cheque book instead of a cheque. You reviewed the amount you saw; the payee gets to write the next page.
saying these in an interview costs you the question
- Assumes approval covers only the reviewed commit
- Thinks the checkout is frozen when the human approves
- Reviews the source diff but not the executable inputs
- Treats a human gate as a substitute for dropping credentials
- Leaves the label attached after a new push