技術約8分で読めます

RubyがGitLabのCVE4件の原因になった正規表現のバグを修正、4.0.9・3.4.12・3.3.13

いけさん目次

TL;DR

影響 外部から受け取った文字列で正規表現を作るRubyアプリ(Regexp.new、Regexp.union、Regexp.compile、/#{input}/)。細工したパターンで、コンパイルや照合の途中にメモリが壊れる
対応 Ruby 4.0.9、3.4.12、3.3.13以降へ更新。更新できない間は、信頼できない入力から正規表現を作らない
対象外 開発者が書いた正規表現しか使わないアプリ。Regexp.timeout やパターンの長さ制限では防げない


2026年10月23日、Rubyの開発チームがRuby 4.0.9、3.4.12、3.3.13を公開した(Regexp compilation fixes related to GitLab’s CVE-2026-89078, CVE-2026-93577, CVE-2026-97229 and CVE-2026-110264)。
Rubyの正規表現エンジンOnigmo(オニグモ、C言語で書かれている)にあった3つのバグを直したリリースで、どれもGitLabが自社のCVEとして公開したリモートコード実行(ネットワーク越しにサーバー上で任意のプログラムを動かされること)の原因になっていた。

GitLab側の経緯は、10月21日に出たCVE-2026-110264の記事(GitLabにCVSS 10.0の脆弱性、.orgファイルの表示から未認証でコード実行)で書いた。
その時点では、GitLabはOmnibus(公式パッケージ)に同梱するRubyへパッチを当てて直しただけで、Ruby本体の修正は開発版のmasterブランチにしか入っていなかった。
今回のリリースで、GitLab以外のRubyアプリも、Ruby本体を上げれば直せるようになった。

GitLabのCVE4件とRubyの修正

GitLabは、ユーザーが書いた正規表現(CI/CD設定のパターンなど)を Regexp.new でコンパイル(パターンの文字列を照合用の内部の命令列に変換すること)している。
細工したパターンでRubyのコンパイル処理のメモリを壊し、そこからGitLabサーバー上のコード実行につなげられることをGitLabが見つけ、9月から10月にかけて4件のCVEとして公開した。

GitLabのCVEGitLabの修正版Ruby側のバグRubyの修正4.0系3.4系3.3系
CVE-2026-8907819.4.1選択(|)の後でパターンの解析に失敗すると、同じメモリを2回解放する(二重解放)#133324.0.03.4.123.3.13
CVE-2026-93577、CVE-2026-9722919.4.1、19.4.2コンパイル後のプログラムのサイズ計算が整数オーバーフローする#190204.0.83.4.123.3.13
CVE-2026-11026419.5.1繰り返しの多いパターンで、内部の番号(ID)があふれる#168014.0.93.4.123.3.13

右の3列は、その修正が最初に入った各系列のバージョン。
4.0.9、3.4.12、3.3.13には、3つの修正が全部入っている。

二重解放の修正#13332は2025年5月にmasterへマージされていて、2025年12月の4.0.0には最初から入っていた。
3.4系と3.3系には入らないまま、今回の3.4.12と3.3.13で初めて取り込まれた。
GitLabの19.4.1の告知(GitLab Critical Patch Release: 19.4.1, 19.3.3, 19.2.7)では、CVE-2026-89078はCI/CD設定の正規表現が入口で、ログイン済みのユーザーがコードを実行できるとしてCVSS 9.9(10点満点の深刻度)になっている。

#19020は、(?:.{90000,}){90000} のような入れ子の繰り返しで、展開後の大きさの掛け算が32ビット整数の上限を超えてしまう問題を直した。
修正前は、制限を超えるはずの繰り返しをそのまま展開しようとして Regexp.new が応答しなくなる。
.{100000,} を数十万文字分つなげると、コンパイル後のプログラムが INT_MAX(約21億)を超え、中のジャンプ先の位置が回り込む(上限を超えた値がマイナスや小さな値に戻る)。

#16801は、前回の記事で扱った繰り返しIDの回り込みを、コンパイルの時点で RegexpError にする修正だ。

3.3系は、脆弱性の修正だけを出すセキュリティメンテナンスの期間に入っている。
告知によると、GitLabがCVEを割り当てたことを理由に、例外として3.3.13にもこれらの修正を入れた。
3.2以前はすでにサポートが終わっていて、修正版は出ていない(Ruby Maintenance Branches)。

影響を受けるRubyアプリ

告知によると、これらのバグが起きるのは、攻撃者が中身を決められる文字列から正規表現をコンパイルするときだけだ。

# 影響を受ける書き方(patternやwordsが外部から来る場合)
Regexp.new(pattern)
Regexp.compile(pattern)
Regexp.union(words)
/#{pattern}/

開発者がソースコードに書いた正規表現しか使わないアプリは対象外になる。
検索フォームの入力やAPIのパラメーター、ユーザーが保存した設定値を、そのまま Regexp.new に入れているような箇所がこれに当たる。

Regexp.timeout(文字列との照合に時間がかかりすぎたら打ち切る設定)は、これらのバグを防げない。
バグの一部は短いパターンでも起きるので、パターンの長さを制限しても確実には防げない、と告知に書かれている。
更新できない場合については、信頼できない入力から正規表現をコンパイルしないように、としか書かれていない。

RubyとしてのCVE番号はまだない

Rubyの開発チームは、今回の修正についてRuby側のCVE番号を取っていない。
Rubyはもともと、信頼できない入力から作った正規表現をコンパイルして使うのは安全でないとしてきた。
それでも、過去の正規表現コンパイルの二重解放(CVE-2022-28738)やメモリの範囲外読み込み(確保した領域の外のメモリを読んでしまうバグ、CVE-2024-27282)は、検討の結果、脆弱性として扱った。

一方で、今の Regexp のドキュメントは、信頼できないパターンをサービス停止(DoS)の問題としてしか書いておらず、タイムアウトの設定を勧めているだけだ。
信頼できないパターンのコンパイルをRubyとして想定するのかははっきり書かれていない。
今後、ドキュメントの書き方と、この種の問題をRubyの脆弱性として扱うか、CVE番号を取るかを話し合うという。
それまでは、脆弱性スキャナーなどで追うときはGitLabのCVE番号を使ってほしい、と告知に書かれている。

5つの版で修正PRのテストパターンを流した

修正が入っているかを確かめるために、各PRで追加された回帰テスト(同じバグの再発を確かめるテスト)のパターンを Regexp.new でコンパイルしてみた。
このPCにはRubyが入っていないので、Windows 11のWSL2(Ubuntu 26.04、x86_64)で、公式のソースからRubyをビルドした。
Docker Hubの公式イメージは、2026年10月23日の夕方の時点で4.0.8まではあり、4.0.9、3.4.12、3.3.13はまだ無かった。

ビルドしたのは、前回の記事でMacのHomebrewで試したのと同じ4.0.6、#19020だけが入った4.0.8、4.0.9、それと3.4.11と3.4.12の5つだ。
ダウンロードしたtar.xzのSHA256は、リリース告知の値と一致した。
正規表現エンジンはRubyの本体に入っているので、拡張ライブラリをビルドしない最小構成の miniruby で試した。

curl -sLO https://cache.ruby-lang.org/pub/ruby/4.0/ruby-4.0.9.tar.xz
sha256sum ruby-4.0.9.tar.xz
tar xf ruby-4.0.9.tar.xz
cd ruby-4.0.9
./configure --disable-install-doc
make -j$(nproc) miniruby
./miniruby -v

流したパターンは5つで、1つずつ別のプロセスで Regexp.new に渡した。
コンパイルのときだけでなく照合のときに起きるバグもあるかを確かめるため、コンパイルが通ったら "xxxxxxxxxx" と照合した。
1つのパターンに30秒以上かかったら強制終了するようにした。

パターン出典のPR
|{1000000}#13332のテスト(二重解放)
(?:.{90000,}){90000}#19020のテスト(入れ子の繰り返し)
.{100000,} を21,475回つなげたもの#19020の説明にある例(プログラムが INT_MAX を超える)
(?:foobar){0,100} を10万回つなげたもの#16801のテスト(繰り返しID)
(?:(?:foo)?|(?:bar)?)* を10万回つなげたもの#16801のテスト(空チェックID)

最初は30秒の打ち切りを timeout の既定のSIGTERM(プロセスに終了を頼むシグナル)で組んでいたが、4.0.6の入れ子の繰り返しが10分以上止まらなかった。
RubyはSIGTERMを受けてもすぐには止まらず、処理の区切りでシグナルを扱うので、コンパイルが終わるまで止まらなかったと考えている。
SIGKILL(プロセスをその場で強制終了させるシグナル)で打ち切るように直して、5つの版を回し直した。

パターン3.4.113.4.124.0.64.0.84.0.9
二重解放(#13332)double free detected で異常終了RegexpErrorRegexpErrorRegexpErrorRegexpError
入れ子の繰り返し(#19020)30秒で打ち切り(2.6GB使用)すぐにコンパイル30秒で打ち切り(2.6GB使用)すぐにコンパイルすぐにコンパイル
INT_MAX 超え(#19020)約5秒でコンパイルが通る(2.1GB使用)RegexpError約6秒でコンパイルが通る(2.1GB使用)RegexpErrorRegexpError
繰り返しID(#16801)照合でSegmentation faultRegexpError照合でSegmentation fault照合でSegmentation faultRegexpError
空チェックID(#16801)エラーなしでコンパイルRegexpErrorエラーなしでコンパイルエラーなしでコンパイルRegexpError

3.4.11では、#13332のテストのパターン |{1000000} を Regexp.new に渡しただけで、Linuxの標準Cライブラリのglibcが二重解放を検出してプロセスが異常終了した。
4.0系は4.0.0から修正が入っているので、4.0.6、4.0.8、4.0.9の3つとも too big number for repeat range の RegexpError になる。

4.0.6と3.4.11では、#16801の繰り返しIDのパターンがコンパイルを通り、10文字の文字列との照合で [BUG] Segmentation fault(許可されていないメモリに触れてプロセスが異常終了すること)になった。
前回の記事のMacではコンパイルが通るところまでしか確かめていなかったが、照合まで進めると異常終了した。

4.0.8は10月22日に出た通常のバグ修正リリースで、#19020の2つのパターンでは修正後の動きになったものの、#16801のパターンでは4.0.6と同じく異常終了した。
#16801の修正は、翌日の4.0.9で入っている。

4.0.9と3.4.12では、5つのパターンすべてがすぐに RegexpError になるか、問題なくコンパイルされた。
入れ子の繰り返しのパターンは、修正後はエラーにならずにすぐコンパイルされ、照合も nil(一致なし)を返す。
Segmentation faultやメモリ破損は、これらのパターンでこのWSL2のx86_64 Linuxに起きたことで、ここからコード実行までは試していない。

更新先

手元のRubyのバージョンは ruby -v で分かる。

今のバージョン更新先
4.0.0〜4.0.84.0.9
3.4.0〜3.4.113.4.12
3.3.0〜3.3.123.3.13
3.2以前修正版なし。サポート中の系列へ移る

3.3.13には、Rubyに標準で同梱されているライブラリ(gem)のresolvを0.3.2に上げて、CVE-2026-80212とCVE-2026-80213を直す修正も入っている(Ruby 3.3.13 Released)。

OmnibusのGitLabは自前のRubyを同梱しているが、ソースからインストールしたGitLabはOSなどに入れたRubyを使う。
その場合も、そのRubyを4.0.9、3.4.12、3.3.13へ上げれば、Ruby側の3つの修正が入る。