CVEReports
CVEReports

Automated vulnerability intelligence platform. Comprehensive reports for high-severity CVEs generated by AI.

Product

  • Home
  • Sitemap
  • RSS Feed

Company

  • About
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 CVEReports. All rights reserved.

Made with love by Amit Schendel & Alon Barad



GHSA-F3FJ-9625-RV3M

GHSA-f3fj-9625-rv3m: Race Condition in Soft Serve Repository Cache Invalidation

Alon Barad
Alon Barad
Software Engineer

Oct 30, 2026·10 min read·3 visits

Executive Summary (TL;DR)

Soft Serve versions >= 0.6.0 and < 0.12.3 contain a cache invalidation race condition in the SetPrivate repository method. Evicting the in-memory LRU cache before committing the database transaction allows concurrent unauthenticated requests to re-cache the stale public state, leaving private repositories perpetually readable by anonymous users. Upgrade to version 0.12.3, which removes the in-memory repository cache entirely.

A Time-of-Check to Time-of-Use (TOCTOU) race condition in Charmbracelet Soft Serve versions 0.6.0 through 0.12.2 allows unauthenticated users to retain read access to repositories after their visibility is changed from public to private. The flaw stems from premature cache invalidation in the SetPrivate method prior to committing the visibility update to the underlying database. Concurrent read requests arriving during the database transaction window repopulate the non-expiring in-memory LRU cache with stale public metadata, bypassing access controls across HTTP, SSH, and Git protocols until the server process is restarted.

Vulnerability Overview

Charmbracelet Soft Serve (github.com/charmbracelet/soft-serve) is a self-hosted Git server accessible over SSH, HTTP, and the native Git daemon protocol (git://). Beginning in version 0.6.0, Soft Serve migrated its repository metadata management from static configuration files to a relational database backend (supporting SQLite and PostgreSQL). To reduce database query overhead during frequent repository lookups, the backend architecture introduced an in-memory Least Recently Used (LRU) cache (pkg/backend/cache.go) built on github.com/hashicorp/golang-lru/v2.

This in-memory cache stored up to 1,000 repository metadata pointers (*repo) and operated without a Time-To-Live (TTL) expiration policy or cache versioning mechanism. When a client initiates a Git operation—such as cloning a repository or polling info/refs—Soft Serve's access control middleware (Backend.AccessLevelForUser) queries Backend.Repository(ctx, name) to retrieve the repository model and inspect its IsPrivate() boolean flag. Because cache hits were trusted unconditionally, the accuracy of access control enforcement depended entirely on strict cache coherence during metadata mutations.

A Time-of-Check to Time-of-Use (TOCTOU) race condition (classified under CWE-362 and CWE-367) exists in the Backend.SetPrivate method in Soft Serve versions >= 0.6.0 and < 0.12.3. When an administrator transitions a repository from public to private, the backend evicts the repository's cache entry before opening and committing the database transaction that updates the persistent record. If a concurrent read request arrives during the execution window between cache eviction and transaction commit, the reader experiences a cache miss, queries the uncommitted database state (private: false), and repopulates the LRU cache with the stale public record.

Because no post-commit cache invalidation occurs and cached records never expire, the repository remains trapped in an inconsistent state where the database reflects private: true, but the in-memory cache serves private: false. Furthermore, in multi-node or multi-process deployments sharing a single database, local cache eviction on one instance fails to invalidate the in-memory caches of peer instances, resulting in deterministic and permanent authorization bypass without requiring a race condition.

Root Cause Analysis

The root cause of GHSA-f3fj-9625-rv3m is the improper ordering of cache eviction relative to database transaction boundaries, combined with the absence of cache synchronization locks or post-transaction invalidation. In a cache-aside architecture, deleting a cache entry prior to updating the backing store creates a classic read-write race window.

When Backend.SetPrivate(ctx, name, true) is invoked, the function immediately executes d.cache.Delete(name). At this exact execution point, the relational database still holds the pre-mutation row where private is false. Before the database record is updated, SetPrivate enters d.db.TransactionContext, performs synchronous filesystem I/O to remove the git-daemon-export-ok marker file from the repository directory, and then executes the SQL UPDATE statement (d.store.SetRepoIsPrivateByName). The inclusion of filesystem syscall operations inside the transaction block widens the timing window between the initial cache deletion and the final database commit.

When a concurrent thread or goroutine calls Backend.Repository(ctx, name) during this window, d.cache.Get(name) returns a cache miss. The reader then initiates its own read transaction via d.store.GetRepoByName, which observes the isolated, pre-commit database state (private: false). Even if the administrator's write transaction commits immediately afterward, the concurrent reader proceeds to execute d.cache.Set(name, r), writing the stale private: false struct pointer back into the LRU cache.

> [!NOTE] > This flaw also introduces an operational blindspot for administrators. When an operator verifies the repository state via the SSH CLI command repo private <name>, the CLI handler queries the database directly rather than consulting the in-memory cache, returning true. Meanwhile, the visibility_change webhook fails to fire because its trigger comparison relies on the stale cached object, leaving administrators unaware that the repository remains publicly accessible.

Code Analysis

The vulnerable caching layer was introduced in commit e398b4dde723437f637b06710e0938d349b360e8 across pkg/backend/cache.go and pkg/backend/repo.go. In the vulnerable implementation of pkg/backend/repo.go, Repository() trusted cached pointers unconditionally, while SetPrivate() invalidated the cache prior to transaction execution:

// Vulnerable code in pkg/backend/repo.go (prior to v0.12.3)
func (d *Backend) Repository(ctx context.Context, name string) (proto.Repository, error) {
	name = utils.SanitizeRepo(name)
 
	// Unconditional cache hit with no TTL or version validation
	if r, ok := d.cache.Get(name); ok && r != nil {
		return r, nil
	}
 
	rp := filepath.Join(d.repoPath(name))
	if _, err := os.Stat(rp); err != nil {
		return nil, proto.ErrRepoNotFound
	}
 
	var m models.Repo
	var err error
	if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
		m, err = d.store.GetRepoByName(ctx, tx, name)
		return err
	}); err != nil {
		return nil, db.WrapError(err)
	}
 
	r := &repo{
		name: name,
		path: rp,
		repo: m,
	}
 
	// Stale database read is written back to cache if raced with SetPrivate
	d.cache.Set(name, r)
	return r, nil
}
 
func (d *Backend) SetPrivate(ctx context.Context, name string, private bool) error {
	name = utils.SanitizeRepo(name)
	rp := filepath.Join(d.repoPath(name))
 
	// BUG: Cache evicted before database transaction starts
	d.cache.Delete(name)
 
	if err := db.WrapError(
		d.db.TransactionContext(ctx, func(tx *db.Tx) error {
			fp := filepath.Join(rp, "git-daemon-export-ok")
			if !private {
				if err := os.WriteFile(fp, []byte{}, fs.ModePerm); err != nil {
					return err
				}
			} else {
				if err := os.Remove(fp); err != nil && !errors.Is(err, fs.ErrNotExist) {
					return err
				}
			}
			return d.store.SetRepoIsPrivateByName(ctx, tx, name, private)
		}),
	); err != nil {
		return err
	}
	// No post-commit cache invalidation performed here
	...
}

To remediate the vulnerability in version 0.12.3, the maintainers authored two commits: 5b307b4da4db285f797cc028b381ba4d3d853019 and 0a4e6f0b7e063927c7128756e1c1b3db871edabf. Rather than attempting to synchronize the cache via locks or double-delete patterns (which would still leave multi-process deployments sharing a database vulnerable to stale local caches), commit 5b307b4da4db285f797cc028b381ba4d3d853019 completely removed pkg/backend/cache.go and eliminated the golang-lru/v2 dependency.

Commit 0a4e6f0b7e063927c7128756e1c1b3db871edabf subsequently refined SetPrivate in pkg/backend/repo.go to query the current database state inside the transaction prior to modifying files or emitting visibility webhooks:

// Patched code in pkg/backend/repo.go (v0.12.3)
func (d *Backend) SetPrivate(ctx context.Context, name string, private bool) error {
	name = utils.SanitizeRepo(name)
	rp := filepath.Join(d.repoPath(name))
 
	var changed bool
	if err := db.WrapError(
		d.db.TransactionContext(ctx, func(tx *db.Tx) error {
			// Verify repository exists and compare state inside transaction
			m, err := d.store.GetRepoByName(ctx, tx, name)
			if err != nil {
				return err
			}
			changed = m.Private != private
 
			fp := filepath.Join(rp, "git-daemon-export-ok")
			if !private {
				if err := os.WriteFile(fp, []byte{}, fs.ModePerm); err != nil {
					return err
				}
			} else {
				if err := os.Remove(fp); err != nil && !errors.Is(err, fs.ErrNotExist) {
					return err
				}
			}
			return d.store.SetRepoIsPrivateByName(ctx, tx, name, private)
		}),
	); err != nil {
		return err
	}
	// Cache is completely eliminated; all reads hit the database directly
	...
}

This remediation is complete and structurally eliminates the bug class. Because SQLite and PostgreSQL metadata queries take tens of microseconds—negligible compared to the process spawning and disk I/O required by underlying Git packfile operations—removing the cache enforces strict transactional consistency across both single-node and multi-node deployments without noticeable performance degradation.

Exploitation

Exploiting GHSA-f3fj-9625-rv3m requires network access to any of the Git transport protocols exposed by Soft Serve (HTTP Smart Transfer, SSH, or the Git daemon) and requires the server's anonymous access configuration (anon-access) to allow read operations, which is the default setting (read-only). No user credentials or SSH keys are needed.

In a single-node environment, an unauthenticated attacker targets a public repository suspected of undergoing privatization—such as a repository accidentally initialized as public during automated provisioning or CI/CD pipeline creation. The attacker runs an automated loop that continuously sends lightweight read requests to the target repository's HTTP endpoint:

GET /target-repo.git/info/refs?service=git-upload-pack HTTP/1.1
Host: git.example.com:23232
User-Agent: git/2.40.0

When an administrator executes the SSH command to restrict repository visibility:

ssh -p 23231 git.example.com repo private target-repo true

If one of the attacker's HTTP requests is processed by the Go HTTP server (server/web/git.go) after SetPrivate calls d.cache.Delete("target-repo") but before the SQL transaction commits, the withAccess middleware invokes be.AccessLevelForUser(ctx, "target-repo", nil), which calls be.Repository(ctx, "target-repo"). This call reads the uncommitted private: false state from the database and stores it in d.cache.

Once the race window is won, the attacker no longer needs to poll. The stale cache entry persists indefinitely in memory. Any subsequent git clone or git fetch over HTTP (server/web/git.go), SSH (server/ssh/git.go), or the Git daemon (server/daemon/daemon.go) will consult be.Repository(), receive the cached private: false struct, and grant ReadOnlyAccess to anonymous clients. This allows the attacker to pull all historical source code as well as any new commits pushed to the repository after it was marked private.

Impact Assessment

The vulnerability is rated Medium severity with a CVSS v3.1 base score of 5.9 (CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N). The Attack Vector is Network (AV:N) with No Privileges Required (PR:N) and No User Interaction (UI:N), as anonymous read access is enabled by default in Soft Serve (anon-access: read-only).

Attack Complexity is rated High (AC:H) because an attacker in a standard single-instance deployment cannot force an administrator to invoke SetPrivate on demand and must win a narrow timing window during the state transition. However, two common operational scenarios lower the practical barrier to exploitation:

  1. Automated Repository Provisioning: Organizations that automate repository creation via scripts often run repo create <name> (which defaults to public) followed immediately by repo private <name> true. If external monitors or webhooks query the repository immediately upon creation, the race condition can be triggered inadvertently.
  2. Multi-Instance Deployments: When multiple Soft Serve processes share a single PostgreSQL or SQLite database, cache invalidation only affects the process handling the SetPrivate command. All other Soft Serve instances that previously cached the public repository will continue serving it to unauthenticated users deterministically (AC:L in practice for those environments).

The Confidentiality Impact is High (C:H). An attacker who triggers or encounters the stale cache state gains full read access to the private repository's Git object database, including all branches, tags, commit histories, source code, and any embedded secrets pushed after the administrator believed the repository was secured. Integrity (I:N) and Availability (A:N) are unaffected, as anonymous access remains restricted to read-only operations.

Remediation

The definitive remediation is to upgrade Charmbracelet Soft Serve to version 0.12.3 or later. Version 0.12.3 removes the in-memory repository LRU cache entirely, ensuring that every access control evaluation queries the authoritative database state directly.

To upgrade a Go module dependency or build from source, run:

go get -u github.com/charmbracelet/soft-serve@v0.12.3

Prebuilt binaries and system packages (.deb, .rpm, .apk) for version 0.12.3 are available directly from the official GitHub release page at https://github.com/charmbracelet/soft-serve/releases/tag/v0.12.3.

If an immediate upgrade to 0.12.3 cannot be performed, operators can apply the following mitigations to eliminate or neutralize stale cache entries:

  • Restart the Soft Serve Service After Visibility Changes: Because the vulnerable cache resides strictly in volatile memory, restarting the Soft Serve process immediately flushes all cached repository records and forces fresh database reads:
    systemctl restart soft-serve
  • Initialize Sensitive Repositories as Private: Avoid creating repositories as public and subsequently running repo private <name> true. Instead, pass the -p flag during initial creation so a public record is never written to the database or cache:
    ssh -p 23231 git.example.com repo create -p confidential-repo
  • Disable Anonymous Access Globally: If public repository hosting is not required, set anon_access to no-access in config.yaml or via the administrative CLI. This enforces authentication checks prior to repository access evaluation:
    ssh -p 23231 git.example.com settings anon-access no-access

Official Patches

CharmbraceletSoft Serve v0.12.3 Official Release
CharmbraceletPrimary patch commit removing the in-memory repository LRU cache
CharmbraceletFollow-up commit fixing repository visibility webhook emission logic

Fix Analysis (3)

Technical Appendix

CVSS Score
5.9/ 10
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N

Affected Systems

github.com/charmbracelet/soft-serve

Affected Versions Detail

Product
Affected Versions
Fixed Version
soft-serve
charmbracelet
>= 0.6.0, < 0.12.30.12.3
AttributeDetail
Advisory IDGHSA-f3fj-9625-rv3m
Vulnerability ClassCWE-362 / CWE-367 (TOCTOU Race Condition)
CVSS v3.1 Score5.9 Medium (CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N)
Affected Versionsgithub.com/charmbracelet/soft-serve >= 0.6.0, < 0.12.3
Patched Version0.12.3
Attack VectorNetwork (HTTP, SSH, and git:// protocols)
Security ImpactUnauthenticated read access to private Git repositories
Exploit StatusProof-of-Concept in unit tests (No KEV listing)

MITRE ATT&CK Mapping

T1068Exploitation for Privilege Escalation
Privilege Escalation
T1213Data from Information Repositories
Collection
T1005Data from Local System
Collection
CWE-362
Concurrent Execution using Shared Resource with Improper Synchronization ('Race Condition')

The product contains a code sequence that can run concurrently with other code, and the code sequence requires temporary, exclusive access to a shared resource, but a timing window exists in which the shared resource can be modified by another code sequence that is operating concurrently.

Vulnerability Timeline

Vulnerable LRU repository cache and SetPrivate invalidation logic introduced in commit e398b4dde723437f637b06710e0938d349b360e8 (PR #337).
2023-07-17
First vulnerable version (v0.6.0) released.
2023-07-25
Fix commits 5b307b4da4db285f797cc028b381ba4d3d853019 and 0a4e6f0b7e063927c7128756e1c1b3db871edabf authored to remove the repository cache and fix visibility webhook logic.
2026-10-03
Patched version v0.12.3 released.
2026-10-05
GitHub Security Advisory GHSA-f3fj-9625-rv3m published.
2026-10-30

References & Sources

  • [1]Soft Serve Security Advisory GHSA-f3fj-9625-rv3m
  • [2]GitHub Advisory Database: GHSA-f3fj-9625-rv3m
  • [3]Fix Commit: Remove in-memory repository cache (5b307b4da4db285f797cc028b381ba4d3d853019)
  • [4]Fix Commit: Send visibility webhook only when visibility changes (0a4e6f0b7e063927c7128756e1c1b3db871edabf)
  • [5]Prior Commit: Implement database module and abstract backend (e398b4dde723437f637b06710e0938d349b360e8)
  • [6]Soft Serve Release v0.12.3
  • [7]Reporter Profile: sondt99

Attack Flow Diagram

Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.

More Reports

•13 minutes ago•GHSA-FGXM-5HVV-X4VH
5.3

GHSA-fgxm-5hvv-x4vh: Private Repository Enumeration via Go Module Discovery and HTTP Response Discrepancies in Soft Serve

Charmbracelet Soft Serve versions 0.5.0 through 0.12.2 contain an authorization bypass and observable response discrepancy vulnerability in the HTTP Git server. Remote attackers can determine the existence, exact names, and clone URLs of private repositories by issuing crafted Go remote import path discovery requests (?go-get=1) or by observing differential HTTP status codes across Git Smart HTTP, Dumb HTTP, and Git LFS endpoints.

Alon Barad
Alon Barad
0 views•12 min read
•about 2 hours ago•CVE-2026-119283
7.5

CVE-2026-119283: Unbounded JWE Recipients Array Causes CPU Exhaustion in Authlib joserfc

A resource consumption vulnerability exists in the Authlib joserfc Python library prior to version 1.7.5. When processing JSON Web Encryption (JWE) objects formatted using General JSON Serialization, the library fails to enforce an upper bound on the number of elements within the recipients array. An unauthenticated attacker can supply a JWE containing thousands of recipient entries targeting an RSA key management algorithm, forcing the server to perform an equivalent number of computationally expensive RSA private-key decryption operations per request and exhausting available CPU resources.

Alon Barad
Alon Barad
2 views•9 min read
•about 3 hours ago•CVE-2026-119284
6.9

CVE-2026-119284: Uncontrolled Resource Consumption in joserfc Compact Deserialization

CVE-2026-119284 (GHSA-9pg5-8c7x-5hfx) is an uncontrolled resource consumption vulnerability (CWE-400) in the compact serialization deserialization routines of joserfc prior to version 1.7.6. The library enforced individual segment size limits only after invoking Python's bytes.split(b'.') on untrusted input tokens. By sending oversized compact JWS or JWE tokens, an unauthenticated remote attacker can force the Python interpreter to duplicate and allocate memory for every segment before size validation executes, resulting in memory exhaustion and an Endpoint Denial of Service (DoS).

Amit Schendel
Amit Schendel
7 views•10 min read
•about 10 hours ago•GHSA-C33X-VCV4-VG3G
2.1

GHSA-c33x-vcv4-vg3g: Missing Slot Identifier Validation in Cedar Policy Language JSON Template Parser Causes Panic and Authorization Divergence

An improper input validation vulnerability (CWE-20) exists in the Entity Syntax Tree (EST) JSON deserialization component of the Cedar Policy Language (`cedar-policy` and `@cedar-policy/cedar-wasm`). When parsing JSON-formatted policy templates that define `is ... in` scope constraints on `principal` or `resource`, the parser used wildcard pattern matching that ignored the provided slot identifier (`?principal` or `?resource`). This omission allows malformed JSON templates to bypass initial deserialization validation, resulting in either a runtime panic during template linking or inverted slot bindings that alter authorization outcomes.

Amit Schendel
Amit Schendel
7 views•10 min read
•about 11 hours ago•GHSA-W9X9-J56R-H98R
8.2

GHSA-w9x9-j56r-h98r: Unauthenticated Confused Deputy Proxy in @eeacms/volto-chatbot

The @eeacms/volto-chatbot package for Plone Volto contains an unauthenticated confused deputy vulnerability (CWE-862) in versions 2.0.3 and earlier. The package registers Express middleware on wildcard proxy routes (`**/_v1_da/**` and `**/_v1_ha/**`) without verifying client authentication or restricting target endpoints. When processing incoming HTTP requests, the middleware automatically attaches server-side administrative credentials before forwarding the requests to upstream Danswer (Onyx) or Halloumi (LLMGW) API servers. This allows unauthenticated network attackers to execute arbitrary administrative API calls against the backend infrastructure.

Amit Schendel
Amit Schendel
6 views•8 min read
•about 13 hours ago•CVE-2026-9705
6.5

CVE-2026-9705: Registration Access Token Authorization Bypass in Keycloak Dynamic Client Registration

CVE-2026-9705 is an insufficient session expiration and authorization bypass vulnerability in Keycloak and Red Hat build of Keycloak. The vulnerability occurs in the OAuth 2.0 Dynamic Client Registration Management Protocol implementation, where the authentication handler fails to verify whether a target client is enabled before accepting requests authenticated with a previously issued Registration Access Token (RAT). Consequently, an actor possessing a valid RAT for an administratively disabled client can query, update, and re-enable the client configuration, restoring revoked access to realm resources.

Amit Schendel
Amit Schendel
8 views•10 min read