On 22 July 2026 the RubyGems team published an advisory, GHSA-9j48-x3c3-mrp2, about a CDN caching bug that could hand one account's legacy API key to a different person for up to an hour. The root-cause fix shipped on 9 July, and on 23 July RubyGems revoked all legacy API keys, notified affected users and disclosed publicly. The bug was reported on 6 July by Luke Marshall of Truffle Security.
RubyGems rated it CVSS 4.0 7.2 (High). There is no confirmed abuse, but the team says its logs cannot reliably tell a real key holder from an intruder.
What's affected
- Users of RubyGems clients older than 3.2.0, which sign in with
GET /api/v1/api_key. That includes the vendored gem 3.0.3.1 at/usr/bin/gemon macOS Tahoe. - Anyone, including people not signing in, who happened to receive another user's key from the cache.
- Per the advisory, 18 percent of
gem signinsign-ins still came from an affected client version.
Scoped keys, including those made through the web UI or CLI, and short-lived trusted-publishing keys were not affected. gem install and bundle install still work anonymously. Published releases cannot be overwritten: repushing an existing version returns HTTP 409.
How the bug worked
The endpoint authenticates with HTTP Basic and creates a new key. The Ruby client sends Accept-Encoding: gzip, so Rack::Deflater compresses the response. Rack's ETag middleware cannot read a gzipped body, so it fell back to a bare Cache-Control: no-cache, without private and without Vary: Authorization.
Fastly treated the 200 response as cacheable under a shared key for a single edge node, for up to an hour. The next caller on that node got the same key. A plain curl request did not reproduce it, because the bug needed the gzip header. The Rack::Deflater change that set up the condition dates to 10 October 2016, and RubyGems 3.2.0 moved sign-in to POST on 10 December 2020.
Timeline
| Date | Event |
|---|---|
| 10 Oct 2016 | Rack::Deflater added, the application-side trigger |
| 10 Dec 2020 | RubyGems 3.2.0 moves gem signin to POST; the GET endpoint stays for older clients |
| 6 Jul 2026 | Reported by Luke Marshall, Truffle Security |
| 9 Jul 2026 | Root-cause fix deployed (commit d3d11c0); affected objects purged from Fastly |
| 22 Jul 2026 | Advisory dated |
| 23 Jul 2026 | All legacy keys revoked, users notified, public disclosure |
What a leaked legacy key could do: push, yank, add or remove owners, change webhooks and configure trusted publishers. What it could not do: change a password, email or MFA setting, or delete the account. An attacker could publish a higher version number, yank versions, or reclaim a name after yanking every version. Because actions appear under the legitimate key holder, the only separating signals are IP address and user agent.
How to check if you're exposed
Review each gem you own:
gem owner your-gem-name
gem list --remote your-gem-name --all | head -20
Then look at the gem's page and your API key history for:
- versions you did not publish, especially a number higher than your latest
- unexpected yanks
- owners or maintainers you do not recognize
- trusted publishers or webhooks you did not configure
Check your CI for stored keys such as RUBYGEMS_API_KEY or GEM_HOST_API_KEY, because those legacy keys now return HTTP 401.
What to do now
- Create a new scoped API key at rubygems.org/profile/api_keys and load it into your config. Use the narrowest scope the job needs.
- Replace keys stored in CI. Jobs that only install gems, or that already use trusted publishing, are fine.
- Audit every gem for changes you did not make. The advisory warns that adding an owner and registering a trusted publisher both persist after the key is revoked, so revoking alone does not undo them.
- Set MFA to
ui_and_api. The advisory says that setting blocks a leaked key from push, yank, owner and trusted-publisher changes. Accounts set todisabledorui_and_gem_signinget no protection. - Upgrade the client:
gem update --system. Old clients can no longer rungem signin, though creating a key on the website works with any version.
How this could have been avoided
- Never cache authenticated responses. The fix was
Cache-Control: private, no-store,Surrogate-Control: max-age=0and addingAuthorizationtoVary. A CDN test that sends an authenticated gzip request twice would have caught this class of bug. - Retire old API endpoints. The vulnerable
GETendpoint existed only for clients from before 2020, and 18 percent of sign-ins still depended on it. - Scope and expire keys. A full-access key that never expires is worth a lot to whoever gets it. Scoped keys, and trusted publishing in CI, cap the damage.
- Require MFA for API actions. With
ui_and_api, a stolen key alone could not push or change owners. - Prefer identity over secrets in CI. Short-lived OIDC credentials were not affected at all.
Why it matters
A registry bug does not need a malicious maintainer. A header missing from a cached response was enough to expose publishing power over Ruby gems.
Two details stand out. The condition behind it dates to 2016, and per the Cloud Security Alliance note the earliest plausible exposure goes back that far. Also, the advisory shows that revoking a key is not the end of cleanup, because owners and trusted publishers added with it live on.
Try it yourself
List every credential that can publish your packages: registry keys, CI secrets, trusted publisher entries. For each, write down its scope, expiry and whether MFA applies. Anything that is full access with no expiry goes to the top of the rotation list.
Click through it at your own pace, or open it full screen.
Quick check
Why is revoking a leaked RubyGems key not enough?
Owners and trusted publishers added with the key persist after revocation, so you must audit and remove them.
Which MFA level protects against a leaked legacy key?
ui_and_api. Accounts set to disabled or ui_and_gem_signin get no such protection.
Why did a plain curl request not reproduce the bug?
The bug required the Accept-Encoding gzip header, which the Ruby client sends by default.