Branch Protection and CODEOWNERS Gaps
- paths with no code owner
- approvals that survive new pushes
- bypass lists and admin overrides
Scenario
An attacker has phished a developer's GitHub session at a fintech company. They want to plant a backdoor in the authentication service without attracting attention.
The main branch requires one approval. CODEOWNERS covers most of the repository, but a new directory, src/main/java/com/mycorp/auth/mfa/, was added last month and nobody updated the file. It also has no entry for .github/workflows/ or for CODEOWNERS itself.
- The attacker opens a PR with a genuine, harmless refactor of the MFA module.
- A colleague on another team approves it quickly.
- After approval, the attacker pushes one more commit. It adds a hard-coded bypass code to the MFA check.
- The branch rule does not dismiss stale approvals, and no code owner is required for that path. The PR merges with the original approval still attached.
Real-world precedent: GitHub's "Require approval of the most recent reviewable push" setting exists to close exactly this post-approval gap (GitHub Docs). The xz-utils backdoor (CVE-2024-3094, March 2024) showed how much damage a trusted account with merge rights can do; see Developer Review Fatigue and Social Engineering.
Reconnaissance
Explanation
An attacker with read access can learn a lot about your review rules. CODEOWNERS is a plain file in the repository. Merged PRs show how many approvals were needed and who usually gives them. The attacker looks for paths without owners, reviewers who approve fast, and whether approvals survive later pushes.
Insight
Your review rules are only as strong as their least-covered path. Attackers look for that path deliberately.
Practical
- Read your own CODEOWNERS as an attacker would. List sensitive paths with no owner.
- Remember that the last matching pattern wins. A later, broader line without owners can remove ownership from an earlier one.
- Look at recent merges: who approved, how fast, and whether commits landed after approval.
Tools/Techniques
- GitHub's CODEOWNERS error check:
gh api repos/OWNER/REPO/codeowners/errors. - The PR timeline (commits after the approval event) in the GitHub UI or
gh pr view --json reviews,commits.
Metrics/Signal
- Sensitive directories with no CODEOWNERS match.
- PRs merged with commits pushed after the last approval.
Evaluate
Explanation
Check the rules that actually apply to main, from both classic branch protection and rulesets. Then check who can bypass them.
Insight
Many organisations have good rules with a long bypass list. Every person and app on that list is a way around review.
Practical
- Fetch the effective rules for a branch:
gh api repos/OWNER/REPO/rules/branches/main
gh api repos/OWNER/REPO/branches/main/protection
- Confirm these settings are on:
- Require a pull request, with at least one approval (two for high-risk repositories).
- Require review from Code Owners.
- Dismiss stale pull request approvals when new commits are pushed.
- Require approval of the most recent reviewable push.
- Require status checks to pass.
- Do not allow bypassing the above settings (classic protection), or an empty or minimal bypass list (rulesets).
- Check that CODEOWNERS has no syntax errors and that every listed owner has write access.
Tools/Techniques
- OpenSSF Scorecard
Branch-ProtectionandCode-Reviewchecks. - OpenSSF Allstar to check branch protection across an organisation continuously.
- GitHub's ruleset insights page, which shows rule evaluations and bypasses.
Metrics/Signal
- Repositories where
mainlacks code-owner review or stale-approval dismissal. - Number of actors on each ruleset bypass list.
Fortify
Explanation
Use organisation-level rulesets so every repository gets the same baseline, and keep bypass lists short. Give CODEOWNERS a default owner, then add specific owners for sensitive paths, workflows and the CODEOWNERS file itself.
Insight
Protecting .github/workflows/ and CODEOWNERS matters most. Without that, an attacker can quietly change who reviews, or what CI runs.
Practical
A CODEOWNERS file with a fallback owner and protected sensitive paths:
# Default owner for everything
* @mycorp/maintainers
# Sensitive code
/src/main/java/com/mycorp/auth/ @mycorp/identity-team
# Pipeline and ownership rules
/.github/workflows/ @mycorp/platform-security
/.github/CODEOWNERS @mycorp/platform-security
- Create an organisation ruleset targeting the default branch of every repository, with:
- Require a pull request with code-owner review.
- Dismiss stale approvals and require approval of the most recent reviewable push.
- Required status checks, including your security scanners.
- Block force pushes and deletions.
- Keep the bypass list empty for
main, or limit it to a break-glass team with audit logging. - Manage rulesets as code so changes themselves get reviewed.
Tools/Techniques
- GitHub rulesets.
- CODEOWNERS reference.
- Terraform GitHub provider for rulesets and repository settings.
Metrics/Signal
- Percentage of repositories covered by the organisation ruleset.
- CODEOWNERS coverage of sensitive paths,
.github/workflows/and CODEOWNERS itself.
Limit
Explanation
If a malicious change does merge, limit how far it travels before a human sees it. Separate merge from deploy, and protect production deployments with their own approval.
Insight
Commit signing tells you which key signed a commit. It does not prove which person or machine made the change. A stolen session or key still produces "verified" commits.
Practical
- Deploy to production through a GitHub environment with required reviewers.
- Keep production credentials scoped to that environment, so a merged change cannot reach them from a PR build.
- Use short-lived sessions and enforce SSO and 2FA for the organisation, to limit what a stolen session can do.
- Keep the ability to edit rulesets with a small group, separate from day-to-day maintainers.
Tools/Techniques
- GitHub environments with required reviewers.
- Organisation SSO and 2FA enforcement.
Metrics/Signal
- Time between merge to
mainand production deployment. - Number of people who can change rulesets or bypass them.
Expose
Explanation
Watch for the events this attack produces: rules being weakened, bypasses being used, and merges where the approved code differs from the merged code.
Insight
Ruleset bypasses and protection changes are rare in a healthy organisation. Each one deserves a look.
Practical
- Alert on audit log events for ruleset and branch protection changes.
- Alert on ruleset bypasses.
- Alert on changes to CODEOWNERS or
.github/workflows/merged without a platform security review. - Periodically report PRs merged with commits after the final approval.
Tools/Techniques
- GitHub audit log streaming for
repository_rulesetandprotected_branchevents. - Ruleset insights for bypass history.
- OpenSSF Allstar issues when a repository drifts from policy.
Metrics/Signal
- Number of ruleset bypasses per month.
- Changes to protection on default branches.
Exercise
Explanation
Try the attack yourself in a sandbox repository that copies your production rules.
Insight
Most teams find at least one path without an owner, or one person who can bypass everything.
Practical
- Copy your production ruleset and CODEOWNERS to a sandbox repository.
- Open a PR touching an unowned path. Get it approved, then push a further commit.
- Try to merge. Record whether the approval was dismissed and whether a code owner was required.
- Try editing CODEOWNERS in a PR. Record who was asked to review.
- Fix the gaps and repeat.
Tools/Techniques
- A sandbox repository and two test accounts.
gh api repos/OWNER/REPO/rules/branches/mainbefore and after.
Metrics/Signal
- Number of gaps found per exercise.
- Whether the post-approval push was blocked after fixes.