TL;DR
Any unauthenticated user could send a GET request to the RubyGems API and be served cached responses that included other users’ valid API tokens.
Retrieving RubyGems API Tokens
The RubyGems API was caching all GET responses to its API endpoints. This included the endpoints that returned API keys, such as:
api_key.jsonapi_keyapi_key.yaml
The prerequisite was that the request had to be sent with the Accept-Encoding header set to either gzip or gzip, deflate, br.
This wasn’t much of a barrier, since most web browsers and HTTP clients set Accept-Encoding to gzip by default. RubyGems’ own CLI package, gem, set this encoding by default and also made a GET request to /api/v1/api_key and /api/v1/me/profile.yaml during the signin process, which meant it was vulnerable.
Here’s how the attack worked using the gem CLI on version 3.2.0 and below. User A and User B were on separate networks, but in the same geographical region (within a few hundred miles).
User A (victim)
Ran
gem signin
In the background, the gem CLI authenticated with basic auth (username:password) and made a GET request to /api/v1/api_key, then stored the returned API key in ~/.gem/credentials.

User A signed in with the gem CLI, which fetched and stored an API token in ~/.gem/credentials.
If the authenticating user didn’t have an API key available, the gem CLI would create an API token automatically, with the following scopes:

With no existing key, the gem CLI silently created a new token with broad default scopes.
User B (attacker)
Ran this command:
curl --compressed "https://rubygems.org/api/v1/api_key"

User B requested the same URL and was served User A’s cached token.
But this didn’t have to be a malicious request — it could happen by accident. If two users authenticated via the gem CLI at the same time, neither had credentials in ~/.gem/credentials, and they shared a POP region, it was likely they would end up sharing an API key.
We tested this and it worked on the first try: using two accounts, we authenticated at the same time while on two different VPNs in the same region.
The result: both accounts shared one API token — with some pretty permissive scopes, we might add.

Two users signing in at once from the same region could be handed the same token.
Where the vulnerability existed
The root cause was a shared cache that keyed on the request URL but not on the Authorization header. Fastly’s edge cached authenticated responses and then served them to anyone requesting the same URL, treating a private, per-user response as if it were a public, cacheable resource.
Because Fastly POPs are metro-region based, an attacker only needed to be reasonably close to the victim, or retry from multiple vantage points until they landed on the same edge node that held the cached response.
This only happened for requests sent with the Accept-Encoding: gzip (or gzip, deflate, br) header, which is exactly what most browsers and HTTP clients send by default.

An authenticated request returned the API key and the edge cached the response (x-cache: MISS on the first fill, keyed on the URL, not on the auth header).

The same endpoint without credentials returned 401, the origin correctly refused whenever the response wasn’t served from cache.
Recreating it locally
We stood up a tiny lab with two pieces:
An origin (a small app) that behaves correctly: it requires a username and password to return the token, sends
Cache-Control: no-cache, and returns401 Unauthorizedif you don’t log in.Varnish sitting in front of it, configured with the bug.
The entire vulnerability lives in a few lines of Varnish config. In plain English it says: “If someone asks for a compressed response, cache it, even if they were logged in, and even if the origin said not to.”
Two things are happening that make this bug possible:
Varnish’s cache key is just the URL, it does not include who you are, so the logged-in user and the anonymous attacker look up the exact same cached item.
The config throws away the origin’s
no-cacheinstruction and stores the response anyway.
Proving it
We fired three curl requests at our local Varnish:
1. The victim (logged in, compressed) fills the cache, this is essentially what the gemCLI does.
A normal, correct, authenticated response, the token was returned and X-Cache: MISS.
2. The attacker sends a request with no basic auth but with the --compressed flag, the cached response is returned.
3. If we omit the --compressed flag, the application behaves as expected, no cached response is returned.
Impact
A RubyGems API token is a supply-chain credential. Depending on the account, it could push new gem versions, yank releases, and manage owned packages. If an affected account maintained a widely used gem, the cache bug became a direct path from an unauthenticated HTTP request to package compromise, on top of full API-level account takeover.

RubyGems hosts over 300,000 packages that have been downloaded a total of 250m+ times.
Disclosure
TODO: Fill in discovery, report submission, vendor acknowledgement, mitigation, verification, and publication dates once the disclosure timeline is finalized.


