Kestrel
대시보드로 돌아가기
CVE-2026-56443MEDIUM· 4.3GHSA대응게시일: 2026. 07. 21.수정일: 2026. 07. 21.

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

위협 신호 · CVSS · EPSS · KEV

정기 패치· 높은 악용 신호 없음
CVSS
4.3medium

이론적 심각도 점수

EPSS

예측 데이터 없음

KEV
미등재

실측 악용 기록 없음

권장 대응 기한60일 이내CISA SSVC 기준

계획된 패치 주기 내 조치(60일 이내)

외부 노출· KEV 미등재 · 자동화 어려움 · 부분 영향 · 외부 노출

CVSS 벡터 · 메트릭

악용 경로
공격 벡터네트워크
공격 복잡도낮음
필요 권한낮음
사용자 상호작용불필요
범위불변
영향
기밀성 영향낮음
무결성 영향없음
가용성 영향없음
버전별 점수
CVSS 3.14.3MODERATE
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N

상세 설명

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:LineFunctionAffected surface
routers/api/v1/api.go:292checkTokenPublicOnly Package caseAPI v1 packages
routers/api/packages/api.go:76reqPackageAccess middlewareAll 24 native package registries (/api/packages/<type>/...)
services/context/api.goTokenCanAccessRepo helperAll API v1 Repository-category endpoints — content, issues, PRs, releases, labels, milestones, etc.
services/context/permission.go:32CheckTokenScopes (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.

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

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

text
1case auth_model.AccessTokenScopeCategoryOrganization:
2 orgPrivate := ... && !ctx.Org.Organization.Visibility.IsPublic() // !IsPublic ✓
3case auth_model.AccessTokenScopeCategoryUser:
4 if ... && !ctx.ContextUser.Visibility.IsPublic() { ... } // !IsPublic ✓
5case auth_model.AccessTokenScopeCategoryActivityPub:
6 if ... && !ctx.ContextUser.Visibility.IsPublic() { ... } // !IsPublic ✓
7
8case auth_model.AccessTokenScopeCategoryPackage:
9 if ctx.Package != nil && ctx.Package.Owner.Visibility.IsPrivate() { // IsPrivate ONLY ✗
10 ctx.APIError(http.StatusForbidden, "token scope is limited to public packages")
11 return
12 }

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

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

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

text
1if publicOnly && repo != nil && repo.IsPrivate {
2 ctx.HTTPError(http.StatusForbidden)
3 return
4}

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-only201.
  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:

bash
1PROBE A (download package via attacker PAT)
2 HTTP=200 Body: secret-content-internal-only
3
4PROBE C (read README of limuser's PUBLIC repo)
5 HTTP=200 Body: {"name":"README.md", ...}
6
7PROBE F (sanity — User category, same PAT, same owner)
8 HTTP=403 Body: {"message":"token scope is limited to public users"}
9
10PROBE G (Repository category, same PAT, same owner)
11 HTTP=200 Body: {"name":"README.md", ...} ← bypass
12
13PROBE H (list limuser's repos, User category)
14 HTTP=403 Body: {"message":"token scope is limited to public users"}
15
16PROBE M (git HTTPS smart protocol — info/refs)
17 HTTP=200 Body: 001e# service=git-upload-pack ... HEAD ... ← full clone enabled
18
19PROBE N (write attempt: POST contents/hacked.txt)
20 HTTP=403 Body: {"message":"user should have a permission to write to the target branch"}
21 Integrity:N confirmed
22
23PROBE O (Limited ORG — same bypass class)
24 Org category : HTTP=403 {"message":"token scope is limited to public orgs"}
25 Repo category : HTTP=200 README content ← bypass
26
27Anonymous 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)

text
1// routers/api/v1/api.go:292 (checkTokenPublicOnly Package case)
2- if ctx.Package != nil && ctx.Package.Owner.Visibility.IsPrivate() {
3+ if ctx.Package != nil && !ctx.Package.Owner.Visibility.IsPublic() {
4
5// routers/api/packages/api.go:76 (reqPackageAccess middleware)
6- if ctx.Package != nil && ctx.Package.Owner.Visibility.IsPrivate() {
7+ if ctx.Package != nil && !ctx.Package.Owner.Visibility.IsPublic() {
8
9// services/context/api.go (TokenCanAccessRepo helper)
10- return repo == nil || !ctx.PublicOnly || !repo.IsPrivate
11+ return repo == nil || !ctx.PublicOnly ||
12+ (!repo.IsPrivate && repo.Owner != nil && repo.Owner.Visibility.IsPublic())
13
14// services/context/permission.go:32 (CheckTokenScopes)
15- if publicOnly && repo != nil && repo.IsPrivate {
16+ if publicOnly && repo != nil &&
17+ (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

AI 심층 분석

공격 시나리오 · 재현 가능한 PoC 페이로드 · 즉시 적용 가능한 차단 패치를 한 번에 받아 보세요. 보안 운영팀이 그대로 점검·티켓팅에 쓸 수 있는 형태로 정리해 드립니다.