2026-08-29 9 min read

Approving your own pull request

Branch protection with required reviews is the control most teams lean on to keep unreviewed code off their main branch. It answers one question: has someone other than the author signed off? For a few years there was a way to make the answer yes without anyone signing off.

I did not find this. Omer Gil at Cider Security published it in 2021.[1] I had been treating required reviews as a hard wall for years, so I wanted to actually watch it fail instead of trusting a blog post. I built the smallest repo that would reproduce it. Here is what came out of that, including the part that still works.

The short version

A pull request could approve itself. The approval came from a token with write access rather than from a person, and the required-review check never told the two apart. The rest of this is why that worked, why GitHub needed two separate changes to close it, and the gap those changes still leave.

What the token actually is

Every workflow run gets a GITHUB_TOKEN. It is an ephemeral credential GitHub mints for that run, scoped to the repo, acting under the identity github-actions[bot]. Not a human. A bot with, potentially, write access.

In April 2021 GitHub shipped the permissions: key,[2] which lets a workflow declare exactly what its token can do:

permissions:
  pull-requests: write

Here is the detail that makes the bypass an insider problem rather than a misconfiguration: anyone who can edit a workflow file can grant this. The org-wide default token permission only governs workflows that do not declare their own permissions: block. A workflow that asks for pull-requests: write gets it, whatever the default says. If you can push a branch and add a .github/workflows/*.yml file, you can hand yourself a writing bot.

Why required reviews fell for it

Branch protection's required-review check is simpler than it looks. It confirms two things about an approval:

  1. 01
    Does the reviewer have write access?
    github-actions[bot] with pull-requests: write does.
  2. 02
    Is the reviewer a different identity than the author?
    The human opened the PR; the bot submits the approval. Different identity.
  3. 03
    Both true, so the approval counts
    Nothing ever asked whether the approver was a person.
What required-review enforcement actually checks

The bot has write access, and it is not the account that opened the pull request. Both boxes tick. The check never asks the question that actually matters, which is whether a person looked at the code. It counts approvals; it does not weigh them.

Reproducing it, and the trigger that trips people up

To reproduce it you need a repo with:

  • Branch protection on main requiring at least one review.
  • No CODEOWNERS rule covering the change (one bot approval cannot satisfy CODEOWNERS, which is exactly why requiring it was the recommended fix).
  • Actions enabled, which they were by default.
  • Write access, to push a branch and add a workflow.

Then a workflow triggered on the pull request calls the GitHub API to submit an approving review on the very PR that triggered it.

If you skip that distinction you end up filing this under the more famous "pwn request" fork-secrets class, which is a different bug with a different fix.

I built the smallest thing that would still feel real: a throwaway repo with a tiny Go service in it, just so a pull request had an actual diff to change instead of an empty commit. Branch protection on main, one required review, Actions left at their defaults. Then I opened a PR by hand, as me, and let a workflow do the approving. The whole workflow is this short:

name: auto-approve
on:
  pull_request:
jobs:
  approve:
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write
    steps:
      - run: gh pr review "$PR_NUMBER" --approve --body "lgtm"
        env:
          GH_TOKEN: ${{ github.token }}
          PR_NUMBER: ${{ github.event.pull_request.number }}

My first run did nothing, and it took me a minute to see why: I had a CODEOWNERS file left over from copying the repo template, and a bot approval does not satisfy CODEOWNERS. That failure is the whole defense in one line, so I deleted the file to watch the undefended case, which is what most repos actually are.

The second run merged. What stuck with me was not that it worked but how ordinary it looked. The timeline showed github-actions[bot] approved these changes, a green check, and the merge button turned from grey to green while I sat there having reviewed nothing. There is no warning, no different color, no "this approval came from automation" note. It reads exactly like a colleague signed off. I had spent years treating that green check as evidence a second person looked, and here it was, printed by a token I told to print it.

GitHub's fix, in two stages

GitHub did not patch this in one move, and conflating the two stages is a common mistake in write-ups.

  1. 01
    Jan 2022, approval gate
    An org setting for whether Actions reviews count toward required approval. It shipped enabled by default, so the hole stayed open unless you knew to close it.
  2. 02
    May 2022, creation gate
    Extended the same toggle to also block Actions from creating PRs. Now one setting: Allow GitHub Actions to create and approve pull requests. [3]
  3. 03
    Feb 2023, read-only default
    The default GITHUB_TOKEN scope itself became read-only for new repos. A second, independent layer. [4]
The remediation, and what each stage actually covered

Today a freshly created repo is safe on two independent layers: Actions cannot approve PRs, and the default token is read-only. The setting lives under Settings → Actions → General → Workflow permissions.

The variant the fix does not touch

The setting gates the github-actions[bot] identity. It does nothing about a personal access token stored in Actions secrets. A PAT belongs to a real user, and to GitHub's enforcement that is a completely different actor.

An independent proof of concept by Jeremy Long uses exactly this:[5] push to a branch, have the workflow create a second branch and PR through the API, approve it with a PAT from secrets, merge, then delete the branches to clean up. The org toggle does nothing here, because the approver is not the bot.

GitHub did not fix this badly. But the fix names an identity, and the question underneath it, whether an approval's authenticity counts for more than its number, is still open. It got patched one actor at a time. The newer controls have the same shape: a separate required-approval policy for Copilot's identity, docs saying Copilot review comments do not count toward required approvals. Each one adds another actor to the trust model. None of them changes how the model works.

What to check on your own repos

Setting aside the specific bug, the trust boundary is still worth auditing:

  • Turn off "Allow GitHub Actions to create and approve pull requests" unless you have a workflow that genuinely needs it. On older orgs this was not flipped retroactively, so a repo created before 2022 may still have it on. Check, do not assume.
  • Require CODEOWNERS review, or two approvals. A single approver of any kind, human or otherwise, is a thinner wall than it looks.
  • Audit PATs in secrets. They sidestep the Actions approval gate entirely. Prefer the scoped GITHUB_TOKEN, or a GitHub App with least privilege, over a long-lived personal token sitting in a secret.
  • Pin permissions: explicitly at the top of every workflow, at the lowest scope that works. Do not rely on the org default, because a malicious workflow ignores it anyway, but an explicit block makes an over-privileged workflow obvious in review.

None of this is exotic. It is the same lesson the original research pointed at: a CI pipeline is code that runs with credentials, and code that runs with credentials is an attack surface, whether or not anyone is currently attacking it.

I reproduced this because I had trusted that green check for years without ever making it lie to me, and it turns out a control you have never watched fail is a control you do not actually understand. The first thing I did after the repo merged itself was open the Actions settings on the repos I care about and read them, instead of assuming.

references

  1. [1]

    Omer Gil, "Bypassing required reviews using GitHub Actions", Cider Security, 2021.

  2. [2]

    GitHub, "Control permissions for GITHUB_TOKEN", GitHub Changelog, 20 April 2021.

  3. [3]

    GitHub, "Prevent GitHub Actions from creating and approving pull requests", GitHub Changelog, 3 May 2022.

  4. [4]

    GitHub, "Updating the default GITHUB_TOKEN permissions to read-only", GitHub Changelog, 2 February 2023.

  5. [5]

    Jeremy Long, "Bypassing-Required-Reviews", GitHub. The PAT-in-secrets variant.

  6. [6]

    "CICD-SEC-1: Insufficient Flow Control Mechanisms", OWASP Top 10 CI/CD Security Risks.

$