KEVWire

Security intelligence, with context.

Response snapshot Oct 30, 2026, 17:36 UTC

Back to the wire
Permalink
VulnerabilityGHSA-fgxm-5hvv-x4vh

charmbracelet/soft-serve HTTP responses disclose the existence and names of private repositories

Soft Serve answers Go's ?go-get=1 requests by walking up the requested path until it finds a repository, then rendering that repository's name and clone URL into a go-import meta tag. The walk checked that each candidate repository existed, but never that the caller could read it.

At a glance

Exploited
Not known
Severity
Not scored yet
Chance of exploitation
Not estimated
Sources
1 source

What to do

GitHub lists first patched versions: github.com/charmbracelet/soft-serve: 0.12.3. Confirm the affected range and upgrade guidance in the exact advisory.

Read GitHub-reviewed advisory GHSA-fgxm-5hvv-x4vh (opens a new tab)

What happened

Summary

Soft Serve answers Go's ?go-get=1 requests by walking up the requested path until it finds a repository, then rendering that repository's name and clone URL into a go-import meta tag. The walk checked that each candidate repository existed, but never that the caller could read it.

A request for a path inside a private repository, such as GET /secret/anything?go-get=1, therefore returned 200 naming secret and its clone URL, even though GET /secret?go-get=1 correctly returned 404. The same ?go-get=1 exception in the HTTP access middleware also let requests reach the LFS and dumb-HTTP routes with the repository existence check skipped, where two LFS handlers dereferenced the missing repository and panicked.

The same disclosure appears without ?go-get=1. The Git and LFS routes answered a repository the caller could not read differently depending on whether it existed: an anonymous clone of a private repository got 401 while a missing one got 404, and a signed-in user without access got the reverse pattern. Either way, one request confirms whether a guessed name is a private repository.

Continue reading

This was found while fixing the go-get issue and is included here because it has the same impact and is fixed in the same release.

Impact

A remote client can learn, with one request per candidate name, which private repositories exist, their exact names, and their clone URLs. Under the shipped default of anon-access: read-only no account is needed, and an authenticated user without access to the repository gets the same answer. Project names in private deployments are often sensitive (internal codenames, unreleased products, customer names), and this turns them into an enumerable namespace.

The Git and LFS routes give the same confirmation for any guessed name, to anonymous and authenticated callers alike, and an invalid Authorization: Bearer header does the same even where credentials are otherwise required.

Repository contents, refs, commits, and collaborators are not exposed, and no write path is reachable. The LFS panic is caught by the recovery middleware and returns a 500; the server stays up. Hidden repositories are not affected beyond what direct access already allows, since hiding a repository removes it from listings but does not restrict reading it by name.

Reachable over the HTTP interface, which is enabled by default (AV:N), with one ordinary GET request per name (AC:L). No privileges are required under the default anonymous read access (PR:N), and no victim interaction is needed (UI:N). Only repository names and clone URLs are disclosed, which is a low confidentiality impact (C:L), with no integrity or availability impact (I:N, A:N), since the panic is recovered per request.

Root Cause

Two pieces of code each assumed the other had checked access.

withAccess in pkg/web/git.go computed the access level for the requested path and then let go-get requests through ahead of the guard meant to hide repositories:

case r.URL.Query().Get("go-get") == "1" && accessLevel >= access.ReadOnlyAccess:
 // Allow go-get requests to passthrough.
 break
case repo == nil, accessLevel < access.ReadOnlyAccess:
 // Don't hint that the repo exists if the user doesn't have access
 renderNotFound(w, r)
 return

For a path that is not itself a repository, AccessLevelForUser returns the anonymous access level to anonymous callers and read-write to any authenticated user, since that is what allows a push to create a new repository. The go-get case therefore always matched for a sub-path, and the "don't hint that the repo exists" guard below it never ran.

GoGetHandler in pkg/web/goget.go then trimmed path segments with path.Dir until be.Repository succeeded, and rendered the first repository it found. It never asked whether the caller could read that repository. The access level the middleware had computed was for a different path.

Because the exception lived in the middleware shared by every Git route rather than on the go-get route, it also applied to the dumb-HTTP and LFS routes. There it skipped the repo == nil guard, and serviceLfsBasicDownload and serviceLfsBasicUpload dereferenced a nil repository from the request context. The sibling LFS handlers already checked for nil, which is why only those two panicked.

The Git and LFS routes behind withAccess had the opposite problem: each branch chose its own response for a caller who could not read the repository, and the choice depended on which condition failed first. On the smart HTTP routes a missing repository was checked before credentials, so it returned 404, while a private one fell through to the credential prompt and returned 401.

On the LFS write routes the write-access check ran before the read check, so a signed-in user got 403 for a private repository and 404 for a missing one. And parseJWT in pkg/web/auth.go rejected a token for a missing repository with a "missing repository" error, which the caller treated as anonymous, while a token for an existing repository failed as invalid and got 403.

Affected Versions

v0.5.0 through v0.12.2. The go-get walk-up without an access check was introduced in commit 3dfecd9 (fix(http): go-get requests), and private repositories arrived in the same v0.5.0 release. The middleware exception that also reached the LFS and dumb-HTTP routes was added in v0.6.0, together with the HTTP LFS routes, and the differing responses on the Git and LFS routes date from the same middleware.

Patches

Fixed in v0.12.3.

The go-get route no longer goes through the per-repository access middleware. It authenticates the caller and nothing more, and GoGetHandler checks read access on each repository it considers during the walk-up. A repository the caller cannot read is skipped exactly as if it did not exist, so an unreadable repository and a missing one produce the same 404, and a readable repository nested under an unreadable one

Affected package ranges reported by GitHub: github.com/charmbracelet/soft-serve: >= 0.5.0, < 0.12.3

Related technology

Sources

  1. Primary source

    GitHub-reviewed advisory GHSA-fgxm-5hvv-x4vh (opens a new tab)

    The public advisory supplies the description, affected package/version ranges, and any listed CVSS metric. GitHub review is not local review.

    Published Checked

  2. Primary source

    GitHub public advisory API page (opens a new tab)

    The API page supplied GHSA-fgxm-5hvv-x4vh, its actual identifiers, source dates, and structured metrics.

    Published Date not suppliedChecked

Cite this record

For factual claims, cite the primary sources above; cite KEVWire for its compilation, dates, and classifications.

APA

KEVWire. (2026, October 30). charmbracelet/soft-serve HTTP responses disclose the existence and names of private repositories. https://kevwire.com/record/GHSA-fgxm-5hvv-x4vh

MLA

"charmbracelet/soft-serve HTTP responses disclose the existence and names of private repositories." KEVWire, 30 Oct. 2026, kevwire.com/record/GHSA-fgxm-5hvv-x4vh.

Chicago

KEVWire. "charmbracelet/soft-serve HTTP responses disclose the existence and names of private repositories." Last modified October 30, 2026. https://kevwire.com/record/GHSA-fgxm-5hvv-x4vh.

Evidence summary (plain text)

charmbracelet/soft-serve HTTP responses disclose the existence and names of private repositories (Oct 30, 2026).
Soft Serve answers Go's ?go-get=1 requests by walking up the requested path until it finds a repository, then rendering that repository's name and clone URL into a go-import meta tag. The walk checked that each candidate repository existed, but never that the caller could read it.

A request for a path inside a private repository, such as GET /secret/anything?go-get=1, therefore returned 200 naming secret and its clone URL, even though GET /secret?go-get=1 correctly returned 404.
Sources:
GitHub-reviewed advisory GHSA-fgxm-5hvv-x4vh: https://github.com/advisories/GHSA-fgxm-5hvv-x4vh
GitHub public advisory API page: https://api.github.com/advisories?type=reviewed&per_page=100&sort=updated&direction=desc
Record ID: GHSA-fgxm-5hvv-x4vh.
Record origin: Ingested record.
AI evidence: Not assessed.
Known exploitation: Not assessed.
Zero-day status: Not assessed. KEV membership alone does not establish zero-day status.
About this data

This record was ingested from a data source. Retrieved: Oct 30, 2026, 17:20 UTC. Ingestion is not an independent editorial investigation.

A CISA KEV listing documents known exploitation, not necessarily a zero-day; absence from the catalog does not prove a vulnerability has never been exploited.

Technology tags indicate possible relevance, not verified exposure. Confirm the affected product, version, and configuration against your own inventory; KEVWire does not scan your systems.

A personal reading checklist on this device, not verified remediation.

Record JSON

Your account

Email alerts

Email alerts are coming as part of KEVWire Premium: join the waitlist. To follow your stack today, use its RSS feed, which honors your filters, or connect an AI agent through the API.

Suggest an improvement

Ideas, missing sources, corrections, and bugs are all welcome. No account needed. Suggestions are reviewed by a person before anything changes.

Record details

Record detail