GHSA-w5pg-649r-p6gg Gitea: Branch Protection Bypass via PR Retargeting Preserves Stale `official` Approval Flag
SummaryGitea does not re-evaluate the `official` flag on existing pull request reviews when a PR's target branch is changed. An attacker with write access to a repository can obtain an `official: true` approval on a PR targeting an unprotected branch, then retarget the PR to a protected branch (e.g., `master`). The approval, which would have been `official: false` if submitted against the protected branch, is preserved and satisfies the protected branch's required approvals, allowing the attacker to merge without legitimate maintainer approval.Confirmed on Gitea **1.25.4** (`1.25.4+41-g96515c0f20`)Vulnerability DetailsRoot CauseWhen a review is submitted on a pull request, Gitea computes the `official` flag by checking whether the reviewer is in the **target branch's** approval whitelist (`IsUserOfficialReviewer` in `models/git/protected_branch.go`). This flag is stored in the database as a boolean on the review record.When a PR's target branch is subsequently changed via `ChangeTargetBranch` (`services/pull/pull.go:218`), the function:Updates `pr.BaseBranch`Recalculates merge feasibility and divergenceDeletes old push commentsCreates a "change target branch" commentBut it does **not**:Re-evaluate `official` on existing reviewsDismiss existing approvalsCheck whether reviewers are in the new target branch's approval whitelistAt merge time, `GetGrantedApprovalsCount` (`models/issues/pull.go:766`) counts reviews where `official = true AND dismissed = false AND type = Approve`. It reads the stored boolean — it does not re-check the whitelist. The stale `official: true` from the unprotected branch satisfies the protected branch's approval requirement.Relevant Code Paths**Review creation** — `services/pull/review.go:SubmitReview` calls `IsOfficialReviewer` against the current `pr.BaseBranch`'s protection rules, stores `official=true/false`**Target branch change** — `services/pull/pull.go:ChangeTargetBranch` modifies `pr.BaseBranch` but does not touch existing reviews**Merge check** — `services/pull/check.go:CheckPullMergeable` → `models/issues/pull.go:GetGrantedApprovalsCount` counts stored `official=true` reviews without re-evaluating against the new branch's whitelistPrerequisitesThe attacker needs:**Write (push) access** to the repository (collaborator with write role, or the ability to create branches — not admin)The ability to create pull requests (standard for any user with push access)A second account (or any non-admin account) to submit the approval on the unprotected branchThe attacker does **not** need:Admin accessTo be in the approval whitelist for the protected branchAny interaction from the branch protection's designated approversProof of ConceptSetupRepository `owner/repo` with branch `master` protected:Required approvals: 1Approval whitelist enabled, containing only user `admin-reviewer`User `attacker` has write access but is **not** in the approval whitelistStepsObserved API Responses**Step 4** — Approval on unprotected branch:**Step 6** — Same approval after retarget to protected master:The `official` flag is unchanged. Under the protected branch's rules, this user's approval should be `official: false`.Impact**Branch protection bypass**: Protected branches with approval whitelists can be merged into without any whitelisted user approving**Privilege escalation**: A user with write-but-not-admin access can effectively nullify the admin-configured approval requirementsSuggested FixRe-evaluate the `official` flag on all existing reviews when a PR's target branch changes. In `services/pull/pull.go:ChangeTargetBranch`, after updating `pr.BaseBranch`:Alternatively, dismiss all existing approvals on retarget (simpler, more conservative):
CVSS v3.1 score: 8.1