GHSA-7P4H-3GXQ-X3H3
Vulnerability from github – Published: 2026-07-21 20:17 – Updated: 2026-07-21 20:17Summary
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:
- Create user
limuser.PATCH /api/v1/admin/users/limuserwith body{"visibility":"limited", ...}— response confirms"visibility":"limited". - Upload a generic package as
limuser:PUT /api/packages/limuser/generic/secretpkg/1.0.0/secret.txtwith bodysecret-content-internal-only→201. - Create user
attacker. - Mint PAT for
attackerwithscopes=["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": [
{
"package": {
"ecosystem": "Go",
"name": "code.gitea.io/gitea"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.27.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-56443"
],
"database_specific": {
"cwe_ids": [
"CWE-863"
],
"github_reviewed": true,
"github_reviewed_at": "2026-07-21T20:17:19Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"details": "## Summary\n\nAfter [PR #37118](https://github.com/go-gitea/gitea/pull/37118) / **CVE-2026-25714**\n(`fix: Unify public-only token filtering in API queries and repo access checks`,\nmerged 2026-05-18, backport `#37773` to 1.26.2 \u2014 the May 2026 unification pass\nfor public-only token filtering, reporter Medoedus per the 1.26.2 release notes),\nthe `public-only` PAT scope is still bypassable on **Repository** and **Package**\nscope categories when the owner\u0027s `Visibility = Limited` (instance-internal).\n\nThe sibling `Org` / `User` / `ActivityPub` cases in the same `checkTokenPublicOnly`\nswitch correctly reject Limited owners via `!Visibility.IsPublic()`. The\nRepository / Package cases use `repo.IsPrivate` or `Owner.Visibility.IsPrivate()`,\nboth of which return `false` for `VisibleTypeLimited` \u2014 so a `public-only` PAT\nstrictly exceeds anonymous reach on a Limited owner.\n\nTested on `gitea/gitea:1.26.2`. The decisive marker is that PR #37118\u0027s\nunification IS applied in the version under test (User-category PROBE returns\n`403 \"token scope is limited to public users\"`). Despite that, the\nRepository-category PROBE on the same Limited owner with the same PAT returns\n`200` and serves content.\n\n## Affected entry points (4 spots)\n\n| File:Line | Function | Affected surface |\n|---|---|---|\n| `routers/api/v1/api.go:292` | `checkTokenPublicOnly` Package case | API v1 packages |\n| `routers/api/packages/api.go:76` | `reqPackageAccess` middleware | All 24 native package registries (`/api/packages/\u003ctype\u003e/...`) |\n| `services/context/api.go` | `TokenCanAccessRepo` helper | All API v1 Repository-category endpoints \u2014 content, issues, PRs, releases, labels, milestones, etc. |\n| `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. |\n\nAll four sinks check `repo.IsPrivate` or `Owner.Visibility.IsPrivate()` only.\n`VisibleTypeLimited` falls through.\n\n```go\n// modules/structs/visible_type.go\nfunc (vt VisibleType) IsPrivate() bool { return vt == VisibleTypePrivate } // line 39-40\nfunc (vt VisibleType) IsLimited() bool { return vt == VisibleTypeLimited } // line 33-34\n```\n\n## Same-file evidence (`routers/api/v1/api.go:246-299` after PR #37118)\n\n```go\ncase auth_model.AccessTokenScopeCategoryOrganization:\n orgPrivate := ... \u0026\u0026 !ctx.Org.Organization.Visibility.IsPublic() // !IsPublic \u2713\ncase auth_model.AccessTokenScopeCategoryUser:\n if ... \u0026\u0026 !ctx.ContextUser.Visibility.IsPublic() { ... } // !IsPublic \u2713\ncase auth_model.AccessTokenScopeCategoryActivityPub:\n if ... \u0026\u0026 !ctx.ContextUser.Visibility.IsPublic() { ... } // !IsPublic \u2713\n\ncase auth_model.AccessTokenScopeCategoryPackage:\n if ctx.Package != nil \u0026\u0026 ctx.Package.Owner.Visibility.IsPrivate() { // IsPrivate ONLY \u2717\n ctx.APIError(http.StatusForbidden, \"token scope is limited to public packages\")\n return\n }\n```\n\n`TokenCanAccessRepo` (`services/context/api.go`) reduces to `!repo.IsPrivate`:\n\n```go\n// A public-only token cannot reach a private repo; any other token is unrestricted by this check.\nfunc (ctx *APIContext) TokenCanAccessRepo(repo *repo_model.Repository) bool {\n return repo == nil || !ctx.PublicOnly || !repo.IsPrivate\n}\n```\n\n`CheckTokenScopes` (`services/context/permission.go:32`):\n\n```go\nif publicOnly \u0026\u0026 repo != nil \u0026\u0026 repo.IsPrivate {\n ctx.HTTPError(http.StatusForbidden)\n return\n}\n```\n\n## PoC (Docker e2e VERIFIED on `gitea/gitea:1.26.2`, 2026-06-05)\n\nFull script in the report (`run-poc.sh`). Setup:\n\n1. Create user `limuser`. `PATCH /api/v1/admin/users/limuser` with body\n `{\"visibility\":\"limited\", ...}` \u2014 response confirms `\"visibility\":\"limited\"`.\n2. Upload a generic package as `limuser`:\n `PUT /api/packages/limuser/generic/secretpkg/1.0.0/secret.txt` with body\n `secret-content-internal-only` \u2192 `201`.\n3. Create user `attacker`.\n4. Mint PAT for `attacker` with `scopes=[\"read:package\",\"read:user\",\"read:repository\",\"public-only\"]`.\n\nResult on `gitea/gitea:1.26.2` \u2014 nine PROBEs:\n\n```\nPROBE A (download package via attacker PAT)\n HTTP=200 Body: secret-content-internal-only\n\nPROBE C (read README of limuser\u0027s PUBLIC repo)\n HTTP=200 Body: {\"name\":\"README.md\", ...}\n\nPROBE F (sanity \u2014 User category, same PAT, same owner)\n HTTP=403 Body: {\"message\":\"token scope is limited to public users\"}\n\nPROBE G (Repository category, same PAT, same owner)\n HTTP=200 Body: {\"name\":\"README.md\", ...} \u2190 bypass\n\nPROBE H (list limuser\u0027s repos, User category)\n HTTP=403 Body: {\"message\":\"token scope is limited to public users\"}\n\nPROBE M (git HTTPS smart protocol \u2014 info/refs)\n HTTP=200 Body: 001e# service=git-upload-pack ... HEAD ... \u2190 full clone enabled\n\nPROBE N (write attempt: POST contents/hacked.txt)\n HTTP=403 Body: {\"message\":\"user should have a permission to write to the target branch\"}\n Integrity:N confirmed\n\nPROBE O (Limited ORG \u2014 same bypass class)\n Org category : HTTP=403 {\"message\":\"token scope is limited to public orgs\"}\n Repo category : HTTP=200 README content \u2190 bypass\n\nAnonymous baseline (no auth) on every above endpoint: HTTP=401/404\n```\n\nGitea\u0027s own server error string in PROBE F / H / O \u2014 *\"token scope is limited\nto public users\"* / *\"public orgs\"* \u2014 is the explicit declaration of intent.\nRepository / Package category violates that intent on the same Limited owner.\n\n## Why this is not a duplicate of CVE-2026-25714\n\nCVE-2026-25714 / PR #37118 (the May 2026 unification pass for public-only token\nfiltering, merged 2026-05-18, backported to 1.26.2 via PR #37773) realigned\n`checkTokenPublicOnly`\u0027s Org / User / ActivityPub cases on `!Visibility.IsPublic()`\nand introduced the `TokenCanAccessRepo` helper for the Repository / Issue /\nNotification cases.\n\nPROBE F on `1.26.2` returns `403 \"token scope is limited to public users\"` for\nthe User category \u2014 i.e. PR #37118\u0027s unification IS in effect on the version\nunder test. The Repository / Package leak occurs *after* that fix; the Limited\ngap is the next residual issue on the same hygiene effort (the Package case was\nnot touched, and `TokenCanAccessRepo` reduces to `!repo.IsPrivate` without\nconsulting owner visibility), not the same bug.\n\n## Suggested fix (4 spots, 1-line shape each)\n\n```go\n// routers/api/v1/api.go:292 (checkTokenPublicOnly Package case)\n- if ctx.Package != nil \u0026\u0026 ctx.Package.Owner.Visibility.IsPrivate() {\n+ if ctx.Package != nil \u0026\u0026 !ctx.Package.Owner.Visibility.IsPublic() {\n\n// routers/api/packages/api.go:76 (reqPackageAccess middleware)\n- if ctx.Package != nil \u0026\u0026 ctx.Package.Owner.Visibility.IsPrivate() {\n+ if ctx.Package != nil \u0026\u0026 !ctx.Package.Owner.Visibility.IsPublic() {\n\n// services/context/api.go (TokenCanAccessRepo helper)\n- return repo == nil || !ctx.PublicOnly || !repo.IsPrivate\n+ return repo == nil || !ctx.PublicOnly ||\n+ (!repo.IsPrivate \u0026\u0026 repo.Owner != nil \u0026\u0026 repo.Owner.Visibility.IsPublic())\n\n// services/context/permission.go:32 (CheckTokenScopes)\n- if publicOnly \u0026\u0026 repo != nil \u0026\u0026 repo.IsPrivate {\n+ if publicOnly \u0026\u0026 repo != nil \u0026\u0026\n+ (repo.IsPrivate || (repo.Owner != nil \u0026\u0026 !repo.Owner.Visibility.IsPublic())) {\n```\n\nThis aligns the Repository / Package categories with the User / Org / ActivityPub\nsiblings already shipped in PR #37118.\n## Reporter\n\nJebeenLee",
"id": "GHSA-7p4h-3gxq-x3h3",
"modified": "2026-07-21T20:17:19Z",
"published": "2026-07-21T20:17:19Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/go-gitea/gitea/security/advisories/GHSA-7p4h-3gxq-x3h3"
},
{
"type": "PACKAGE",
"url": "https://github.com/go-gitea/gitea"
},
{
"type": "WEB",
"url": "https://github.com/go-gitea/gitea/releases/tag/v1.27.0"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "Gitea: Token public-only scope bypassed on Limited-visibility owners (Repository + Package categories) \u2014 residual after CVE-2026-25714 / PR #37118"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.