Safeguard
Supply Chain Security•

RubyGems revokes every legacy API key after a CDN cache leak

A caching bug let one account's RubyGems API key be served to another caller for up to an hour. On 23 July 2026 all legacy keys were revoked. What to check on your gems.

Safeguard Security Team
6 min read

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/gem on 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 signin sign-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

DateEvent
10 Oct 2016Rack::Deflater added, the application-side trigger
10 Dec 2020RubyGems 3.2.0 moves gem signin to POST; the GET endpoint stays for older clients
6 Jul 2026Reported by Luke Marshall, Truffle Security
9 Jul 2026Root-cause fix deployed (commit d3d11c0); affected objects purged from Fastly
22 Jul 2026Advisory dated
23 Jul 2026All 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

  1. 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.
  2. Replace keys stored in CI. Jobs that only install gems, or that already use trusted publishing, are fine.
  3. 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.
  4. 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 to disabled or ui_and_gem_signin get no protection.
  5. Upgrade the client: gem update --system. Old clients can no longer run gem 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=0 and adding Authorization to Vary. A CDN test that sends an authenticated gzip request twice would have caught this class of bug.
  • Retire old API endpoints. The vulnerable GET endpoint 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.

Sources

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.