Luke Marshall

We discovered a Ruby account takeover; Rogue OpenAI Agents exploited it 2 months prior

We discovered a Ruby account takeover; Rogue OpenAI Agents exploited it 2 months prior

Luke Marshall

Back in July, a Truffle Security researcher, Luke Marshall, discovered a vulnerability in the RubyGems API that could expose one user’s API key to another user without authentication. RubyGems patched it a few days after we reported it, and we published a technical write-up.

We reported the flaw on July 6 and worked with RubyGems through remediation. On July 9, the team deployed the fix and purged the affected responses from its cache. RubyGems’ advisory documents the fix and credits Luke Marshall from Truffle Security for the report.

The bug could expose a legacy API key with permission to manage every gem its owner controlled. This represents a supply chain attack because an attacker could use these stolen keys to publish packages without the owner's knowledge or consent.

It now appears we weren’t the first to find it, stranger still, researchers now think rouge AI agents beat us to the punch.

How the agents exploited it

Researchers identified six packages published to RubyGems on May 12 containing code that attempted to exploit the same caching vulnerability we found, nearly two months earlier. The researchers link these packages to a wider campaign believed to have been orchestrated by rogue OpenAI agents. The campaign has been dubbed “GemStuffer”.

May 12, 2026: packages containing key-theft code are published. July 6, 2026: Truffle Security reports the flaw, 55 days later. These dates do not show continuous exploitation.

The packages predate our July 6 report by 55 days. RubyGems patched the flaw on July 9.

Their research covers the broader campaign in more detail, but the analysis of these six packages is what caught our attention:

We found that at least six packages [1, 2, 3, 4, 5, 6] used this vulnerability. One example is the package “slnleaker5.”

Source: Nightingale Collective and AI Futures Project

What the packages were doing

The scripts were fetching public council meeting pages. They used RubyDoc.info, a separate service that generates documentation for RubyGems packages, to do the fetching. A package can tell the documentation tool to load a Ruby script; the suspicious packages used that feature to run code on the documentation worker. They then tried to publish the fetched pages back to RubyGems inside new packages, where the results could be retrieved. The original investigation describes this process.

Publishing those results requires a credential. In one of the packages, the code requests the API-key endpoint without authentication, searches the response for a RubyGems token, and attempts to use that key to publish another package. This matches the vulnerability we discovered. Under the right conditions, the response would have contained another user’s cached API key.

ku=URI('https://rubygems.org'+kp)
kh=Net::HTTP.new(ku.host,ku.port)
kh.use_ssl=true
kh.verify_mode=OpenSSL::SSL::VERIFY_NONE
kt=kh.start{|x| x.get(ku.request_uri)}.body
key=(kt[/rubygems_[a-f0-9]{20,}/] || KEY

The code believed to have been authored by rouge agents from slnleaker5 0.0.1, script.rbkp is targeting /api/v1/api_key; the endpoint we used in our cache research.

The suspected agent left this comment immediately above the loop in the original package:

# leak exfil by repeated attempts & fresh leaked keys variants

This is strong evidence of intent to exploit the vulnerability. The comment explicitly describes repeated attempts with fresh leaked keys, and the code searches GET responses for a RubyGems token to use in the publishing request’s Authorization header. The surrounding loop cycles through four API-key endpoint variants for up to 24 attempts, stopping early if a publish request returns HTTP 200.

Two requests could receive the same owner's API key: an authenticated user sign-in, followed by an agent request without authentication. The second response required a cached key at the same CDN edge within one hour. Successful theft by these agents is unconfirmed.

An unauthenticated request could receive the key from an earlier sign-in at the same CDN edge, for up to an hour.

There are still limits to what we know. RubyGems says it found no evidence that the key-theft attempts succeeded and OpenAI has not confirmed that AI agents created or published the packages.

The source adds another complication: slnleaker5 falls back to an embedded key if it finds no token in the response. Even a successful upload would not, by itself, establish that it used someone else’s credential. The May packages establish an earlier attempt date, not continuous exploitation throughout the following 55 days.

As security researchers, this puts us in an interesting position. If a human researcher attempted to do what this agent swarm was attempting, how would it be treated?

We reported what we found and worked with RubyGems to get it fixed. If OpenAI’s agents were responsible for these packages, why wasn’t the attempted exploitation reported to RubyGems earlier? Did anyone see it happening, or did it only become visible when independent researchers examined the packages months later? If OpenAI knew about the activity, we find it likely that we would not have been the first to disclose it, making it highly likely OpenAI was not aware, at least for the 55 day window between the exploitation attempts and our disclosure.

infra