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

Gitea: Fork-PR Actions task can read a third private repository via the collaborative-owner branch (missing fork-PR guard)

위협 신호 · CVSS · EPSS · KEV

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

이론적 심각도 점수

EPSS

예측 데이터 없음

KEV
미등재

실측 악용 기록 없음

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

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

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

CVSS 벡터 · 메트릭

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

상세 설명

Summary

GetActionsUserRepoPermission (models/perm/access/repo_permission.go) decides whether an Actions
task token may access a target repo. Its cross-repo branches each enforce a fork-PR discriminator —
except the collaborative-owner branch, which is missing the !task.IsForkPullRequest guard that
its sibling has. As a result, when a private repo B lists owner A as a collaborative owner, an
attacker-controlled fork pull-request workflow whose base repo is owned by A is granted code-read
on B — i.e. the fork's YAML can clone a third private repository it has no rights to.

Details

text
1// models/perm/access/repo_permission.go (v1.26.2), in GetActionsUserRepoPermission
2if checkSameOwnerCrossRepoAccess(ctx, taskRepo, repo, task.IsForkPullRequest) { // passes isForkPR -> denies forks
3 return maxPerm, nil
4}
5...
6if taskRepo.IsPrivate { // <-- NO IsForkPullRequest check here
7 actionsUnit := repo.MustGetUnit(ctx, unit.TypeActions)
8 if actionsUnit.ActionsConfig().IsCollaborativeOwner(taskRepo.OwnerID) {
9 return maxPerm, nil // grants code-read to target repo B
10 }
11}

The sibling same-owner path correctly denies fork PRs:

text
1func checkSameOwnerCrossRepoAccess(ctx, taskRepo, targetRepo, isForkPR bool) bool {
2 if isForkPR {
3 return false // Fork PRs are never allowed cross-repo access to other private repositories.
4 }
5 ...
6}

taskRepo = the repo whose workflow is running (the PR's base repo A); repo = the target being
cloned (B). IsCollaborativeOwner(taskRepo.OwnerID) asks "does target B's Actions config trust A's
owner for cross-repo read?" When B trusts ownerA, the branch returns maxPerm (code-read) even when
task.IsForkPullRequest is true
— i.e. when the executing YAML is the fork's, not A's.

Every sibling enforces the fork-PR discriminator; except for this branch:
checkSameOwnerCrossRepoAccess denies forks; ComputeTaskTokenPermissions
(models/actions/token_permissions.go) only clamps the token ceiling to read-only for fork/cross-repo
(its own comment notes the access decision is in GetActionsUserRepoPermission, so it does not
neutralize the gap — it just makes the leak read-only); secrets (models/secret/secret.go) and the
approval gate (services/actions/notifier_helper.go) both correctly key on IsForkPullRequest.

Reachability — the runner clones target repo B over git-HTTP with the task token:
routers/web/repo/githttp.goGetDoerRepoPermission(ctx, repoB, ActionsUser)
GetActionsUserRepoPermission(ctx, repoB, actionsUser, taskID) with IsForkPullRequest == true
collaborative-owner branch returns code-read → p.CanAccess(Read, code) passes → private clone of B
succeeds. (CheckRepoScopedToken in githttp is a no-op for the Actions token.)

PoC

Setup: private base repo A (usera/repoA), private third repo
B (userb/repoB) with a planted SECRET.txt, B's Actions config trusting usera as a collaborative
owner, and a genuine running fork-PR task token (token_hash computed with Gitea's own HashToken)
presented as HTTP Basic. Requesting GET /userb/repoB.git/info/refs?service=git-upload-pack:

Condition (same fork-PR token)HTTPMeaning
anonymous (no token)401auth required
token, A public, B trusts A404branch gated on taskRepo.IsPrivate ⇒ A public skips it
token, A private, B has no collab-owner config404no trust ⇒ denied
token, A private, B trusts A (collab-owner)200git clone of private B succeeds
config removed / restored404 / 200deterministic

In the 200 case, git clone of private repo B succeeded and yielded its SECRET.txt — the full source
of a third private repo the fork-PR author has no rights to.

Impact

Read-only confidentiality breach: discloses the full source of a third private repository (B) to an
untrusted external fork-PR author. Read-only, not write/RCE.

Preconditions (honest):

  1. B is deliberately configured with a collaborative owner — but that is exactly the feature's intended
    use, so realistic for any deployment using it.
  2. The fork PR's base repo A is itself private (the branch is gated on taskRepo.IsPrivate). Forking a
    private A already requires read on A, so this is a normal internal-contributor situation, not a
    weakening — the escalation is "read A (granted) → read a different private repo B (never granted)."
  3. The fork-PR workflow must actually run — most realistically via an attacker who had one earlier PR
    approved (the "approved before" path in ifNeedApproval), after which fork PRs auto-run.

Suggested remediation

Add the same fork-PR guard the sibling path has (one line):

text
1if taskRepo.IsPrivate && !task.IsForkPullRequest {
2 actionsUnit := repo.MustGetUnit(ctx, unit.TypeActions)
3 if actionsUnit.ActionsConfig().IsCollaborativeOwner(taskRepo.OwnerID) {
4 return maxPerm, nil
5 }
6}

This flips Vuln_ForkPR_LeaksThirdPrivateRepo to PASS, keeps Control_NonFork_Allowed PASS
(legitimate collaborative-owner sharing still works), and leaves the existing
TestGetActionsUserRepoPermission suite all green.

AI 심층 분석

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