CVE-2026-58416: Gitea Fork-PR Actions Task Can Read Vulnerability

MODERATE
Advisory Identifier: GHSA-fj8v-hjwv-qm88
CVSS 6.3
CWE-863 CWE-280
Published
Advisory Summary

CVE-2026-58416 is a MODERATE (CVSS 6.3) fork-pr actions task can read vulnerability (CWE-863: Incorrect Authorization) affecting Gitea libraries in the Go ecosystem. The issue involves fork-pr actions task can read a third private repository via the collaborative-owner branch (missing fork-pr guard). Remediation is available by upgrading affected packages to version 1.27.0.

CVE-2026-58416 at a Glance

CVE Identifier CVE-2026-58416
GHSA Identifier GHSA-fj8v-hjwv-qm88
Severity MODERATE
CVSS v3.1 Base Score 6.3
CWE Classification CWE-863: Incorrect Authorization
Affected Product Gitea
Package Ecosystem Go
Published Date
Last Updated

Security Impact

CVSS: 6.3

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

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

How to Fix CVE-2026-58416

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.

gitea.dev fixed: 1.27.0
Upgrade gitea.dev 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 gitea.dev

How to Check if You Are Affected

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

inspect dependencies for gitea.dev

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 `GetActionsUserRepoPermission` (`models/perm/access/repo_permission.go`) decides whether an Actions task token may access a target repo. Its cross-repo branches each enforce a fork-PR discriminator — **except the collaborative-owner branch**, which is missing the `!task.IsForkPullRequest` guard that its sibling has. As a result, when a private repo **B** lists owner **A** as a collaborative owner, an **attacker-controlled fork pull-request** workflow whose base repo is owned by A is granted code-read on B — i.e. the fork's YAML can clone a third private repository it has no rights to. ### Details ```go // models/perm/access/repo_permission.go (v1.26.2), in GetActionsUserRepoPermission if checkSameOwnerCrossRepoAccess(ctx, taskRepo, repo, task.IsForkPullRequest) { // passes isForkPR -> denies forks return maxPerm, nil } ... if taskRepo.IsPrivate { // <-- NO IsForkPullRequest check here actionsUnit := repo.MustGetUnit(ctx, unit.TypeActions) if actionsUnit.ActionsConfig().IsCollaborativeOwner(taskRepo.OwnerID) { return maxPerm, nil // grants code-read to target repo B } } ``` The sibling same-owner path correctly denies fork PRs: ```go func checkSameOwnerCrossRepoAccess(ctx, taskRepo, targetRepo, isForkPR bool) bool { if isForkPR { return false // Fork PRs are never allowed cross-repo access to other private repositories. } ... } ``` `taskRepo` = the repo whose workflow is running (the PR's base repo A); `repo` = the target being cloned (B). `IsCollaborativeOwner(taskRepo.OwnerID)` asks "does target B's Actions config trust A's owner for cross-repo read?" When B trusts ownerA, the branch returns `maxPerm` (code-read) **even when `task.IsForkPullRequest` is true** — i.e. when the executing YAML is the fork's, not A's. Every sibling enforces the fork-PR discriminator; except for this branch: `checkSameOwnerCrossRepoAccess` denies forks; `ComputeTaskTokenPermissions` (`models/actions/token_permissions.go`) only clamps the token *ceiling* to read-only for fork/cross-repo (its own comment notes the access *decision* is in `GetActionsUserRepoPermission`, so it does not neutralize the gap — it just makes the leak read-only); secrets (`models/secret/secret.go`) and the approval gate (`services/actions/notifier_helper.go`) both correctly key on `IsForkPullRequest`. **Reachability** — the runner clones target repo B over git-HTTP with the task token: `routers/web/repo/githttp.go` → `GetDoerRepoPermission(ctx, repoB, ActionsUser)` → `GetActionsUserRepoPermission(ctx, repoB, actionsUser, taskID)` with `IsForkPullRequest == true` → collaborative-owner branch returns code-read → `p.CanAccess(Read, code)` passes → private clone of B succeeds. (`CheckRepoScopedToken` in githttp is a no-op for the Actions token.) ### PoC Setup: private base repo A (`usera/repoA`), private third repo B (`userb/repoB`) with a planted `SECRET.txt`, B's Actions config trusting `usera` as a collaborative owner, and a genuine running fork-PR task token (`token_hash` computed with Gitea's own `HashToken`) presented as HTTP Basic. Requesting `GET /userb/repoB.git/info/refs?service=git-upload-pack`: | Condition (same fork-PR token) | HTTP | Meaning | |---|---|---| | anonymous (no token) | 401 | auth required | | token, A **public**, B trusts A | 404 | branch gated on `taskRepo.IsPrivate` ⇒ A public skips it | | token, A private, B has **no** collab-owner config | 404 | no trust ⇒ denied | | **token, A private, B trusts A (collab-owner)** | **200** | **`git clone` of private B succeeds** | | config removed / restored | 404 / 200 | deterministic | In the 200 case, `git clone` of private repo B succeeded and yielded its `SECRET.txt` — the full source of a third private repo the fork-PR author has no rights to. ### Impact Read-only confidentiality breach: discloses the full source of a *third* private repository (B) to an untrusted external fork-PR author. Read-only, not write/RCE. Preconditions (honest): 1. B is deliberately configured with a collaborative owner — but that is exactly the feature's intended use, so realistic for any deployment using it. 2. The fork PR's base repo A is itself private (the branch is gated on `taskRepo.IsPrivate`). Forking a private A already requires read on A, so this is a normal internal-contributor situation, not a weakening — the escalation is "read A (granted) → read a *different* private repo B (never granted)." 3. The fork-PR workflow must actually run — most realistically via an attacker who had one earlier PR approved (the "approved before" path in `ifNeedApproval`), after which fork PRs auto-run. ### Suggested remediation Add the same fork-PR guard the sibling path has (one line): ```go if taskRepo.IsPrivate && !task.IsForkPullRequest { actionsUnit := repo.MustGetUnit(ctx, unit.TypeActions) if actionsUnit.ActionsConfig().IsCollaborativeOwner(taskRepo.OwnerID) { return maxPerm, nil } } ``` This flips `Vuln_ForkPR_LeaksThirdPrivateRepo` to PASS, keeps `Control_NonFork_Allowed` PASS (legitimate collaborative-owner sharing still works), and leaves the existing `TestGetActionsUserRepoPermission` suite all green.

CVE-2026-58416 Affected Gitea Versions

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

References

Frequently Asked Questions

What is CVE-2026-58416?
CVE-2026-58416 is a security vulnerability in Gitea involving Fork-PR Actions task can read a third private repository via the collaborative-owner branch (missing fork-PR guard) under CWE-863: Incorrect Authorization.
Which Gitea versions are affected?
Affected versions include releases prior to 1.27.0 across affected packages: gitea.dev.
What version fixes CVE-2026-58416?
Upgrading to 1.27.0 (or the latest patched release for your respective branch) resolves the vulnerability.
Is gitea.dev affected?
Yes, gitea.dev 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-58416?
CVE-2026-58416 has a CVSS v3.1 base score of 6.3 (MODERATE) with vector string CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:N/A:N.
What CWE is associated with CVE-2026-58416?
CVE-2026-58416 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-58416?
Run the dependency tree command: 'inspect dependencies for gitea.dev'. 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-fj8v-hjwv-qm88",
  "modified" : "2026-07-21T20:40:03Z",
  "published" : "2026-07-21T20:40:03Z",
  "aliases" : [ "CVE-2026-58416" ],
  "summary" : "Gitea: Fork-PR Actions task can read a third private repository via the collaborative-owner branch (missing fork-PR guard)",
  "details" : "### Summary\n\n`GetActionsUserRepoPermission` (`models/perm/access/repo_permission.go`) decides whether an Actions\ntask token may access a target repo. Its cross-repo branches each enforce a fork-PR discriminator —\n**except the collaborative-owner branch**, which is missing the `!task.IsForkPullRequest` guard that\nits sibling has. As a result, when a private repo **B** lists owner **A** as a collaborative owner, an\n**attacker-controlled fork pull-request** workflow whose base repo is owned by A is granted code-read\non B — i.e. the fork's YAML can clone a third private repository it has no rights to.\n\n### Details\n\n```go\n// models/perm/access/repo_permission.go (v1.26.2), in GetActionsUserRepoPermission\nif checkSameOwnerCrossRepoAccess(ctx, taskRepo, repo, task.IsForkPullRequest) { // passes isForkPR -> denies forks\n    return maxPerm, nil\n}\n...\nif taskRepo.IsPrivate {                                   // <-- NO IsForkPullRequest check here\n    actionsUnit := repo.MustGetUnit(ctx, unit.TypeActions)\n    if actionsUnit.ActionsConfig().IsCollaborativeOwner(taskRepo.OwnerID) {\n        return maxPerm, nil                              // grants code-read to target repo B\n    }\n}\n```\n\nThe sibling same-owner path correctly denies fork PRs:\n\n```go\nfunc checkSameOwnerCrossRepoAccess(ctx, taskRepo, targetRepo, isForkPR bool) bool {\n    if isForkPR {\n        return false // Fork PRs are never allowed cross-repo access to other private repositories.\n    }\n    ...\n}\n```\n\n`taskRepo` = the repo whose workflow is running (the PR's base repo A); `repo` = the target being\ncloned (B). `IsCollaborativeOwner(taskRepo.OwnerID)` asks \"does target B's Actions config trust A's\nowner for cross-repo read?\" When B trusts ownerA, the branch returns `maxPerm` (code-read) **even when\n`task.IsForkPullRequest` is true** — i.e. when the executing YAML is the fork's, not A's.\n\nEvery sibling enforces the fork-PR discriminator; except for this branch:\n`checkSameOwnerCrossRepoAccess` denies forks; `ComputeTaskTokenPermissions`\n(`models/actions/token_permissions.go`) only clamps the token *ceiling* to read-only for fork/cross-repo\n(its own comment notes the access *decision* is in `GetActionsUserRepoPermission`, so it does not\nneutralize the gap — it just makes the leak read-only); secrets (`models/secret/secret.go`) and the\napproval gate (`services/actions/notifier_helper.go`) both correctly key on `IsForkPullRequest`.\n\n**Reachability** — the runner clones target repo B over git-HTTP with the task token:\n`routers/web/repo/githttp.go` → `GetDoerRepoPermission(ctx, repoB, ActionsUser)` →\n`GetActionsUserRepoPermission(ctx, repoB, actionsUser, taskID)` with `IsForkPullRequest == true` →\ncollaborative-owner branch returns code-read → `p.CanAccess(Read, code)` passes → private clone of B\nsucceeds. (`CheckRepoScopedToken` in githttp is a no-op for the Actions token.)\n\n### PoC\n\nSetup: private base repo A (`usera/repoA`), private third repo\nB (`userb/repoB`) with a planted `SECRET.txt`, B's Actions config trusting `usera` as a collaborative\nowner, and a genuine running fork-PR task token (`token_hash` computed with Gitea's own `HashToken`)\npresented as HTTP Basic. Requesting `GET /userb/repoB.git/info/refs?service=git-upload-pack`:\n\n| Condition (same fork-PR token) | HTTP | Meaning |\n|---|---|---|\n| anonymous (no token) | 401 | auth required |\n| token, A **public**, B trusts A | 404 | branch gated on `taskRepo.IsPrivate` ⇒ A public skips it |\n| token, A private, B has **no** collab-owner config | 404 | no trust ⇒ denied |\n| **token, A private, B trusts A (collab-owner)** | **200** | **`git clone` of private B succeeds** |\n| config removed / restored | 404 / 200 | deterministic |\n\nIn the 200 case, `git clone` of private repo B succeeded and yielded its `SECRET.txt` — the full source\nof a third private repo the fork-PR author has no rights to.\n\n### Impact\n\nRead-only confidentiality breach: discloses the full source of a *third* private repository (B) to an\nuntrusted external fork-PR author. Read-only, not write/RCE.\n\nPreconditions (honest):\n1. B is deliberately configured with a collaborative owner — but that is exactly the feature's intended\n   use, so realistic for any deployment using it.\n2. The fork PR's base repo A is itself private (the branch is gated on `taskRepo.IsPrivate`). Forking a\n   private A already requires read on A, so this is a normal internal-contributor situation, not a\n   weakening — the escalation is \"read A (granted) → read a *different* private repo B (never granted).\"\n3. The fork-PR workflow must actually run — most realistically via an attacker who had one earlier PR\n   approved (the \"approved before\" path in `ifNeedApproval`), after which fork PRs auto-run.\n\n### Suggested remediation\n\nAdd the same fork-PR guard the sibling path has (one line):\n\n```go\nif taskRepo.IsPrivate && !task.IsForkPullRequest {\n    actionsUnit := repo.MustGetUnit(ctx, unit.TypeActions)\n    if actionsUnit.ActionsConfig().IsCollaborativeOwner(taskRepo.OwnerID) {\n        return maxPerm, nil\n    }\n}\n```\n\nThis flips `Vuln_ForkPR_LeaksThirdPrivateRepo` to PASS, keeps `Control_NonFork_Allowed` PASS\n(legitimate collaborative-owner sharing still works), and leaves the existing\n`TestGetActionsUserRepoPermission` suite all green.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:N/A:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Go",
      "name" : "gitea.dev"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "1.27.0"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/go-gitea/gitea/security/advisories/GHSA-fj8v-hjwv-qm88"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/go-gitea/gitea/pull/38214"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/go-gitea/gitea/commit/1d43b736b5a16c5f80cfdcd9a9448a9c983ddaa0"
  }, {
    "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-280", "CWE-863" ],
    "severity" : "MODERATE",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-07-21T20:40:03Z",
    "nvd_published_at" : null
  }
}