Skip to content

GHSA-7p4h-3gxq-x3h3 CVE-2026-56443 GO-2026-6045

Gitea: Token public-only scope bypassed on Limited-visibility owners (Repository + Package categories) — residual after CVE-2026-25714 / PR #37118

Critical Go
Severity
4.3 / 10
Critical · CVSS 3.1
Exploit chance (EPSS)
0.53%
Chance of exploitation in the next 30 days. Higher than 44.51% 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

After PR #37118 / CVE-2026-25714 (fix: Unify public-only token filtering in API queries and repo access checks, merged 2026-05-18, backport #37773 to 1.26.2 — the May 2026 unification pass for public-only token filtering, reporter Medoedus per the 1.26.2 release notes), the public-only PAT scope is still bypassable on Repository and Package scope categories when the owner's Visibility = Limited (instance-internal).

The sibling Org / User / ActivityPub cases in the same checkTokenPublicOnly switch correctly reject Limited owners via !Visibility.IsPublic(). The Repository / Package cases use repo.IsPrivate or Owner.Visibility.IsPrivate(), both of which return false for VisibleTypeLimited — so a public-only PAT strictly exceeds anonymous reach on a Limited owner.

Tested on gitea/gitea:1.26.2. The decisive marker is that PR #37118's unification IS applied in the version under test (User-category PROBE returns 403 "token scope is limited to public users"). Despite that, the Repository-category PROBE on the same Limited owner with the same PAT returns 200 and serves content.

Affected entry points (4 spots)

File:Line Function Affected surface
routers/api/v1/api.go:292 checkTokenPublicOnly Package case API v1 packages
routers/api/packages/api.go:76 reqPackageAccess middleware All 24 native package registries (/api/packages/<type>/...)
services/context/api.go TokenCanAccessRepo helper All API v1 Repository-category endpoints — content, issues, PRs, releases, labels, milestones, etc.
services/context/permission.go:32 CheckTokenScopes (called via CheckRepoScopedToken) Web download endpoints /raw, /media, /attachments. LFS routes (services/lfs/server.go:470/472, services/lfs/locks.go:62/151/216/284) also chain through this helper.

All four sinks check repo.IsPrivate or Owner.Visibility.IsPrivate() only. VisibleTypeLimited falls through.

// modules/structs/visible_type.go
func (vt VisibleType) IsPrivate() bool { return vt == VisibleTypePrivate }   // line 39-40
func (vt VisibleType) IsLimited() bool { return vt == VisibleTypeLimited }   // line 33-34

Same-file evidence (routers/api/v1/api.go:246-299 after PR #37118)

case auth_model.AccessTokenScopeCategoryOrganization:
    orgPrivate := ... && !ctx.Org.Organization.Visibility.IsPublic()        // !IsPublic ✓
case auth_model.AccessTokenScopeCategoryUser:
    if ... && !ctx.ContextUser.Visibility.IsPublic() { ... }                // !IsPublic ✓
case auth_model.AccessTokenScopeCategoryActivityPub:
    if ... && !ctx.ContextUser.Visibility.IsPublic() { ... }                // !IsPublic ✓

case auth_model.AccessTokenScopeCategoryPackage:
    if ctx.Package != nil && ctx.Package.Owner.Visibility.IsPrivate() {     // IsPrivate ONLY ✗
        ctx.APIError(http.StatusForbidden, "token scope is limited to public packages")
        return
    }

TokenCanAccessRepo (services/context/api.go) reduces to !repo.IsPrivate:

// A public-only token cannot reach a private repo; any other token is unrestricted by this check.
func (ctx *APIContext) TokenCanAccessRepo(repo *repo_model.Repository) bool {
    return repo == nil || !ctx.PublicOnly || !repo.IsPrivate
}

CheckTokenScopes (services/context/permission.go:32):

if publicOnly && repo != nil && repo.IsPrivate {
    ctx.HTTPError(http.StatusForbidden)
    return
}

PoC (Docker e2e VERIFIED on gitea/gitea:1.26.2, 2026-06-05)

Full script in the report (run-poc.sh). Setup:

  1. Create user limuser. PATCH /api/v1/admin/users/limuser with body {"visibility":"limited", ...} — response confirms "visibility":"limited".
  2. Upload a generic package as limuser: PUT /api/packages/limuser/generic/secretpkg/1.0.0/secret.txt with body secret-content-internal-only → 201.
  3. Create user attacker.
  4. Mint PAT for attacker with scopes=["read:package","read:user","read:repository","public-only"].

Result on gitea/gitea:1.26.2 — nine PROBEs:

PROBE A  (download package via attacker PAT)
    HTTP=200  Body: secret-content-internal-only

PROBE C  (read README of limuser's PUBLIC repo)
    HTTP=200  Body: {"name":"README.md", ...}

PROBE F  (sanity — User category, same PAT, same owner)
    HTTP=403  Body: {"message":"token scope is limited to public users"}

PROBE G  (Repository category, same PAT, same owner)
    HTTP=200  Body: {"name":"README.md", ...}                    ← bypass

PROBE H  (list limuser's repos, User category)
    HTTP=403  Body: {"message":"token scope is limited to public users"}

PROBE M  (git HTTPS smart protocol — info/refs)
    HTTP=200  Body: 001e# service=git-upload-pack ... HEAD ...   ← full clone enabled

PROBE N  (write attempt: POST contents/hacked.txt)
    HTTP=403  Body: {"message":"user should have a permission to write to the target branch"}
                                                                    Integrity:N confirmed

PROBE O  (Limited ORG — same bypass class)
    Org category    : HTTP=403  {"message":"token scope is limited to public orgs"}
    Repo category   : HTTP=200  README content                  ← bypass

Anonymous baseline (no auth) on every above endpoint: HTTP=401/404

Gitea's own server error string in PROBE F / H / O — "token scope is limited to public users" / "public orgs" — is the explicit declaration of intent. Repository / Package category violates that intent on the same Limited owner.

Why this is not a duplicate of CVE-2026-25714

CVE-2026-25714 / PR #37118 (the May 2026 unification pass for public-only token filtering, merged 2026-05-18, backported to 1.26.2 via PR #37773) realigned checkTokenPublicOnly's Org / User / ActivityPub cases on !Visibility.IsPublic() and introduced the TokenCanAccessRepo helper for the Repository / Issue / Notification cases.

PROBE F on 1.26.2 returns 403 "token scope is limited to public users" for the User category — i.e. PR #37118's unification IS in effect on the version under test. The Repository / Package leak occurs after that fix; the Limited gap is the next residual issue on the same hygiene effort (the Package case was not touched, and TokenCanAccessRepo reduces to !repo.IsPrivate without consulting owner visibility), not the same bug.

Suggested fix (4 spots, 1-line shape each)

// routers/api/v1/api.go:292  (checkTokenPublicOnly Package case)
- if ctx.Package != nil && ctx.Package.Owner.Visibility.IsPrivate() {
+ if ctx.Package != nil && !ctx.Package.Owner.Visibility.IsPublic() {

// routers/api/packages/api.go:76  (reqPackageAccess middleware)
- if ctx.Package != nil && ctx.Package.Owner.Visibility.IsPrivate() {
+ if ctx.Package != nil && !ctx.Package.Owner.Visibility.IsPublic() {

// services/context/api.go  (TokenCanAccessRepo helper)
- return repo == nil || !ctx.PublicOnly || !repo.IsPrivate
+ return repo == nil || !ctx.PublicOnly ||
+     (!repo.IsPrivate && repo.Owner != nil && repo.Owner.Visibility.IsPublic())

// services/context/permission.go:32  (CheckTokenScopes)
- if publicOnly && repo != nil && repo.IsPrivate {
+ if publicOnly && repo != nil &&
+     (repo.IsPrivate || (repo.Owner != nil && !repo.Owner.Visibility.IsPublic())) {

This aligns the Repository / Package categories with the User / Org / ActivityPub siblings already shipped in PR #37118.

Reporter

JebeenLee

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