GHSA-p4mj-98mv-xq26 Gitea: Private Repository Existence Disclosure via go-get Meta Endpoint
| Field | Value |
|-------|-------|
| **Affected File** | `routers/web/repo/githttp.go`, `services/context/repo.go` |
| **Affected Functions** | `httpBase()`, `EarlyResponseForGoGetMeta()` |
| **Affected Lines** | `githttp.go:63–66`, `services/context/repo.go:374–396` |
| **Prerequisite** | None — fully unauthenticated |DescriptionGitea 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 privateThe requesting user is authenticatedThe requesting user has any permission on the repositoryThe entry point is `routers/web/repo/githttp.go:63–66`:The `EarlyResponseForGoGetMeta` function (`services/context/repo.go:379–396`) is called unconditionally, and the function's own docstring documents the intended behavior: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:The **full repository name** and owner — confirming the repository existsThe **HTTP clone URL** — a fully-formed URL pointing to the repositoryThe **source browsing URL templates** — which may reveal the default branch nameThis allows an unauthenticated attacker to:**Confirm existence** of any private repository by name**Enumerate** private repository names through brute-force without triggering authentication failures**Harvest** clone URLs and default branch names of private repositoriesProof 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****Actual response (HTTP 200):**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****Step 5 — Verify the same applies to the main web router**The vulnerability also exists via the standard web router for repository pages:All return HTTP 200 with the metadata.Impact Analysis**Direct impact:**| What is leaked | Sensitivity |
|----------------|-------------|
| Repository exists | Confirms presence of private infrastructure code, internal tooling, unreleased products |
| Owner / organization name | Reveals organizational structure |
| Clone URL | Provides a direct endpoint for credential-stuffing attacks against git HTTP endpoint |
| Default branch name | Reduces brute-force surface for subsequent attacks |Root Cause AnalysisThe 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 FixThe fix requires differentiating between requests that carry authentication credentials and those that do not, before calling `EarlyResponseForGoGetMeta`.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.
CVSS v3.1 score: 5.3