Mediumghsa:GHSA-fgxm-5hvv-x4vh

    github.com/charmbracelet/soft-serve: Observable Discrepancy (ghsa:GHSA-fgxm-5hvv-x4vh)

    Published Sources: Hersteller

    Share

    What is affected, what to do

    Update github.com/charmbracelet/soft-serve to version 0.12.3.

    Not sure whether and how you are affected? Ask a question about this vulnerability

    Metrics

    —

    Priority

    No value is available for this vulnerability.

    —

    CVSS v3

    No value is available for this vulnerability.

    —

    EPSS score

    No value is available for this vulnerability.

    Show all metrics
    Severity
    Medium
    Priority (0–325)

    The priority score is a consolidated metric that condenses several individual signals into a single, action-guiding ranking: technical severity (CVSS), exploitation likelihood (EPSS), known-exploited status (KEV) and the context of the affected asset. The aim is to answer the only truly important question in vulnerability management: what first?

    Read in glossary
    30.00 Methodology
    Exploit maturity

    An exploit is code or a technique that specifically uses a concrete vulnerability to force unintended behaviour — such as executing code or escalating privileges. It is the tool that turns a theoretical flaw into a practical attack.

    Read in glossary
    no public PoC known
    KEV status

    The KEV catalogue is a list maintained by the US authority CISA of vulnerabilities for which active exploitation in the wild has been confirmed. It answers not the question „how severe?“ but „is it already being attacked?“ — thereby sharply prioritising patching.

    Read in glossary
    —
    Published
    CWE

    CWE is a catalogue of typical weakness types in software and hardware — such as flawed input validation, unsafe memory handling or poor authentication. Where a CVE names a concrete vulnerability in a concrete product, a CWE describes the underlying class of defect.

    Read in glossary
    CWE-203, CWE-425, CWE-863

    Classification under NIS2 and CRA

    Rule-based classification from exploitation status and severity. It does not replace a case-by-case assessment.

    NIS2

    NIS2 is EU Directive (EU) 2022/2555 to raise the level of cybersecurity across the Union. It substantially broadens obligations compared with the first NIS directive: more sectors, mandatory risk management, staged reporting duties and personal accountability of senior management.

    Read in glossary
    Handle the vulnerability in regular vulnerability handling under Art. 21(2)(e) NIS2.
    CRA

    The CRA is EU Regulation (EU) 2024/2847 setting horizontal cybersecurity requirements for „products with digital elements“ — from industrial controllers and software to connected consumer devices. For the first time manufacturers must ensure security across the entire product lifecycle, handle vulnerabilities and provide security updates.

    Read in glossary
    From 11.12.2027, Annex I Part II CRA requires manufacturers of affected products with digital elements to handle vulnerabilities without delay and provide security updates.
    ISO/IEC 27001

    ISO/IEC 27001 is the internationally leading standard for an information security management system (ISMS). It describes a risk-based, process-oriented framework to systematically plan, implement, review and improve information security.

    Read in glossary
    Control A.8.8, management of technical vulnerabilities: assess relevance to your systems, plan remediation and keep evidence.

    Description

    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. 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 still resolves.

    The go-get exception is removed from withAccess entirely. Every Git, dumb-HTTP, and LFS route now enforces the repository existence and access checks regardless of the query string. The two LFS basic handlers also check for a missing repository, as their siblings did, so a future routing mistake returns a 404 rather than a panic.

    AccessLevelForUser used to prefer the repository attached to the request over the name it was asked about. During the walk-up those differ, so the access check now always looks the repository up by the name it is given.

    Bad credentials on a go-get request now return 403, the same as on the Git routes. Previously they were treated as anonymous for paths that were not themselves repositories.

    On the Git and LFS routes, any repository the caller cannot read now gets one answer, decided in one place: 403 when the credentials are wrong, 401 with a credential prompt when the caller is anonymous, and 404 when they are signed in. This matches how GitHub responds. The LFS write checks now run after the read check, and a JWT presented for a missing repository is rejected as an invalid token like any other.

    Two behaviours change as a result. An anonymous clone of a mistyped repository name now prompts for credentials instead of reporting that the repository does not exist, and a signed-in user without access to a private repository now gets "not found" instead of a credential prompt.

    Pushing over HTTP to a name that does not exist still creates the repository, so a user allowed to create repositories can still tell a taken name from a free one by trying to push. That is inherent to push-to-create and is not changed here.

    Workarounds

    None. Upgrade to the patched release.

    Disabling the HTTP server (http.enabled: false) removes the exposure, at the cost of HTTP Git access. Setting anon-access to no-access limits the disclosure to authenticated users.

    Source: GitHub Security Advisories

    Affected products

    The following versions are affected. Older releases of the same product line are also vulnerable unless stated otherwise.

    Affected products
    Affected versionsFixed in
    gogithub.com/charmbracelet/soft-serve
    • >= 0.5.0, < 0.12.3
    • 0.12.3

    Vendors and products

    Weakness classes (CWE)

    • CWE-203Observable Discrepancy

      Base

      The product behaves differently or sends different responses under different circumstances in a way that is observable to an unauthorized actor.

      Entry at MITRE CWE-203
    • CWE-425Direct Request ('Forced Browsing')

      Base

      The web application does not adequately enforce appropriate authorization on all restricted URLs, scripts, or files.

      Entry at MITRE CWE-425
    • CWE-863Incorrect Authorization

      Class

      The product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check.

      Entry at MITRE CWE-863