2026年10月23日、Rubyの開発チームはRuby 4.0.9・3.4.12・3.3.13を公開しました。正規表現エンジンOnigmoの不具合を直す更新で、GitLabはこの不具合をCVE-2026-97229(CVSS 10.0)など4件のCVEとして採番しています。97229は10月29日に米CISAの悪用済み脆弱性カタログ(KEV)へ追加されました。影響はGitLabに限りません。外部から受け取った文字列で正規表現を組み立てるRubyアプリなら、どれも対象になります。この記事では、4件の中身、影響する版と修正版、Railsアプリで該当箇所を洗い出す手順、更新を待てない場合の書き換えを、実装者の目線で整理しました。内容は2026年10月31日時点の公開情報にもとづきます。
まとめ:Rubyを4.0.9・3.4.12・3.3.13へ上げ、外部入力の正規表現を洗い出す
対応の本線はRuby本体の更新です。4.0系は4.0.9、3.4系は3.4.12、3.3系は3.3.13が修正版で、Railsやgemの更新では直りません。3.2系以前はサポートが終わっており、修正版は出ていません。
影響するのは、Regexp.new・Regexp.union・Regexp.compile、または正規表現リテラルへの式展開に、攻撃者が操作できる文字列が入るコードです。開発者がソースに書いた正規表現だけを使うアプリは影響を受けないと、公式が明記しています。
Regexp.timeoutやパターン長の制限では防げません。更新の日程がすぐに取れないなら、該当箇所を洗い出し、正規表現を使わない書き方へ一時的に置き換えます。社外に公開しているRubyアプリの版と実装をまとめて点検したい場合は、脆弱性診断・セキュリティ診断で範囲を決めると漏れが出にくくなります。
2026年10月のRuby脆弱性4件:Onigmoの不具合とGitLabが採番したCVE
今回の4件は、どれもRubyが正規表現をコンパイルする段階の欠陥です。マッチの計算量が膨らむReDoSとは種類が違い、細工したパターンを読み込ませるだけでメモリが壊れます。
二重解放・整数オーバーフロー・ID溢れの3種類と修正PRの対応
ruby-lang.orgの告知は、4件のCVEを3本の修正に対応づけています。
| CVE | 中身 | Ruby側の修正 | GitLabのCVSS |
|---|---|---|---|
| CVE-2026-89078 | 選択(|)の後に解析が失敗したときの二重解放 | #13332 | 9.9 |
| CVE-2026-93577 | コンパイル後のプログラムサイズの整数オーバーフロー | #19020 | 9.9 |
| CVE-2026-97229 | 同上(GitLab側の別経路) | #19020 | 10.0 |
| CVE-2026-110264 | 繰り返しの多いパターンでの内部IDの溢れ | #16801 | 10.0 |
#13332は、量指定子の回数が大きすぎてエラーになったとき、同じノードを解析と後始末の両方で解放していた欠陥を直しました。分類はCWE-415(二重解放)です。#19020は、繰り返しを展開するかを決めるサイズ計算でint同士の掛け算が溢れ、上限を超えた繰り返しが通ってしまう問題を扱います。#16801は、範囲繰り返しの数が32,767を超えると内部IDが負の値に折り返し、マッチ時に範囲外へ書き込む問題です。後の2本はCWE-190(整数オーバーフロー)に当たります。
GitLabでCVSS 10.0・KEV登録となったCVE-2026-97229の経路
GitLabでは、CI/CD設定に書かれた正規表現をサーバー側のRubyがコンパイルします。NVDのCVE-2026-97229は、認証の無い利用者が細工した正規表現でGitLabサーバー上の任意のコードを実行できたと説明しています。修正は10月12日のGitLab 19.4.2・19.3.5・19.2.8で出ました。
CISAのKEVには10月29日に載り、是正期限は11月1日です。KEVの注記には「同じRuby/Onigmoの整数オーバーフローで他の製品も影響を受けうる」と書かれています。CISAがGitLab以外のRubyアプリも視野に入れている点は見逃せません。攻撃面では、CI Lint経由でこの欠陥を突くMetasploitのモジュール追加PRが公開済みです。NVDに載るCISAの評価(SSVC)は「悪用あり・自動化可能」で、追加から3日という短い期限はこの評価と符合します。期限の決まり方はBOD 26-04の是正期限の決め方で解説しています。
もう1件のCVE-2026-110264はGitLab 7.0以降の全系列が対象で、10月22日の19.5.1ほかのパッチリリースで直りました。GitLabを自社運用している場合の更新手順は、GitLab脆弱性の対応手順と修正版の判断基準にまとめています。CVSSの各指標の読み方はCVSSv3のスコアの目安と計算式で確認できます。
Ruby本体にCVEが無く、脆弱性スキャナが見逃しやすい理由
4件のCVEは、どれもGitLabが自社製品の欠陥として採番したものです。Ruby側は10月31日時点で独自のCVEを申請しておらず、告知は「当面はGitLabのCVE番号で追跡してほしい」としています。
このため、製品名とCVEを突き合わせる脆弱性スキャナは、Rubyの実行環境を対象として検出しない可能性があります。コンテナイメージやサーバーをスキャンして何も出なかったとしても、Rubyの版は自分で確かめてください。4.0.8のリリース告知は「定例のバグ修正」とだけ書いており、#19020が入っていることは読み取れません。
Rubyの正規表現コンパイルが脆弱性として扱われたのは初めてではありません。2024年のCVE-2024-27282も、正規表現まわりのメモリ読み出しの欠陥でした。
影響を受ける版と修正版:4.0.9・3.4.12・3.3.13とEOLの3.2系
修正が入った版はブランチごとに違います。4.0系だけは途中の版で一部が直っているため、版の読み違いに注意が要ります。
ブランチごとの影響版と修正版の対応表とRuby 4.0.8の残存
公式の告知が示す「修正を含む最初の版」を、ブランチごとに並べると次のとおりです。
| ブランチ | CVE-2026-89078 | 93577・97229 | 110264 | 上げる先 |
|---|---|---|---|---|
| 4.0系 | 4.0.0から修正済み | 4.0.8 | 4.0.9 | 4.0.9 |
| 3.4系 | 3.4.12 | 3.4.12 | 3.4.12 | 3.4.12 |
| 3.3系 | 3.3.13 | 3.3.13 | 3.3.13 | 3.3.13 |
| 3.2系以前 | 修正版なし(サポート終了) | 3.3.13以上へ移行 | ||
10月22日に4.0.8へ上げた環境は、CVE-2026-110264がまだ残っています。翌23日の4.0.9まで上げてください。3.3系は本来セキュリティ修正だけを受ける段階ですが、今回はGitLabがCVEを採番したことを理由に例外として3.3.13が出ました。Rubyのブランチ一覧では、3.3系の終了は2027年3月31日の予定です。
本番・CI・コンテナで動くRubyの版を環境ごとに判定するスクリプト
開発者の手元のruby -vは、本番の版と一致するとは限りません。次のスクリプトを本番のサーバーやコンテナの中で実行し、動いている版が修正版以上かを判定します。
# fixed_ruby.rb:実行中のRubyが2026年10月の正規表現修正を含むかを判定する
FIXED = { "4.0" => "4.0.9", "3.4" => "3.4.12", "3.3" => "3.3.13" }.freeze
current = Gem::Version.new(RUBY_VERSION)
branch = current.segments.first(2).join(".")
fixed = FIXED[branch]
if RUBY_ENGINE != "ruby"
puts "#{RUBY_ENGINE} #{RUBY_VERSION}: CRubyではない。処理系ごとの告知を確認する"
elsif fixed.nil?
puts "#{RUBY_VERSION}: 修正版が出ていない系列(サポート終了かプレビュー版)"
elsif current >= Gem::Version.new(fixed)
puts "#{RUBY_VERSION}: 修正済み"
else
puts "#{RUBY_VERSION}: 未修正。#{fixed}以上へ上げる"
end
コンテナならdocker compose exec web ruby fixed_ruby.rb、Kubernetesならkubectl execで同じファイルを渡します。Rubyのソースを書き換えずに済むよう、判定はRuby標準のGem::Versionで行っています。版の数字を文字列のまま比べると「4.0.10」と「4.0.9」の大小が逆になるためです。
版を上げたら、.ruby-version、Gemfileのruby指定、Gemfile.lockのRUBY VERSION、Dockerfileのベースイメージのタグも同じ版に揃えてください。どれか1つが古いままだと、次のデプロイで旧版に戻ります。rbenvを使った版の切り替えはrbenvの仕組みと導入手順、各系列の違いとサポート期限はRubyの最新バージョンとサポート期限で確認できます。
EOLの3.2系以前を使うアプリで修正が届かない場合の移行判断
3.2系は2026年4月1日にサポートが終わりました。今回の修正は3.2系には出ていません。3.2系のまま外部入力から正規表現を組むアプリは、修正の手段がない状態です。
私たちの判断はこうです。外部入力から正規表現を組む箇所が1つでもあれば、3.3.13以上への移行を今回の作業として進めます。移行が終わるまでは、次の章の手順でその箇所を洗い出し、正規表現を使わない書き方へ置き換えてしのぎます。該当箇所が1つも無いと確かめられたなら、移行は通常の更新計画に回して構いません。
影響するコードの条件:Regexp.new・union・式展開に外部入力が入る箇所
公式が挙げる条件は「攻撃者が制御する文字列から正規表現をコンパイルすること」です。正規表現を照合する対象の文字列が外部入力でも、パターン自体が固定なら対象になりません。
影響がある書き方と影響がない書き方、Rails実装での典型例
Railsアプリで外部入力がパターンに入りやすいのは、次のような箇所です。
- 検索画面の「正規表現で探す」機能で、
params[:q]をRegexp.newへ渡している - 前方一致の検索で、
/\A#{params[:prefix]}/のように式展開している - 管理画面から登録した入力チェック用のパターンやNGワードを、DBから読んで
Regexp.newしている - マルチテナントのSaaSで、テナントの管理者が通知やルーティングの条件を正規表現で書ける
管理画面の設定値も外部入力として扱います。権限を持つ利用者なら安全という前提は、アカウントの乗っ取りやテナント間の権限差を考えると成り立ちません。逆に、validates :code, format: { with: /\A[A-Z]{3}\z/ }のように開発者が書いた固定パターンは影響を受けません。
Regexp.unionは、文字列を渡すとエスケープしてから連結するとRegexpの公式ドキュメントに書かれています。公式の告知がRegexp.unionも挙げているのは、外部入力から作ったRegexpオブジェクトを渡すとそのまま連結されるためと読めます。Regexp.unionの引数に何が入るかまで追ってください。
Regexp.timeoutとパターン長の制限が今回の対策にならない理由
Regexp.timeoutはRuby 3.2で入った仕組みで、マッチに時間がかかりすぎたときに処理を打ち切ります。打ち切るのはマッチの段階です。今回の欠陥はコンパイルの段階でメモリを壊すため、タイムアウトが働く前に被害が起きます。公式の告知も、Regexp.timeoutはこれらの不具合を防がないと明記しています。
パターン長の制限も確実ではありません。公式は、一部の不具合は短いパターンでも起きると書いています。{n,m}の回数指定を入れ子にすれば、数十文字でも展開後のサイズは大きくなります。ReDoS対策として入れたタイムアウトや長さの上限は残してよいものの、今回の件では効果を当てにできません。ReDoSとの違いはReDoSのバックトラック爆発と対策の実測で整理しています。
Railsアプリで外部入力から正規表現を組む箇所を洗い出す手順
洗い出しは、粗く拾うgrepと、構文木で絞るスクリプトの2段で進めます。grepで件数の当たりを付け、件数が多いリポジトリだけ2段目を使えば十分です。
grepで当たりを付ける2つのコマンドと検索結果からの誤検知の除き方
Railsアプリのルートで次の2本を実行します。1本目はRegexpのクラスメソッド呼び出し、2本目は式展開を含む正規表現リテラルを拾います。
grep -rnE 'Regexp\.(new|union|compile)\b' --include='*.rb' --include='*.erb' --include='*.rake' app lib config
grep -rnE '(%r\{|/)[^/]*#\{' --include='*.rb' --include='*.erb' app lib config
2026年10月31日に、該当する書き方と該当しない書き方を混ぜたサンプルのコントローラで実行し、Regexp.new・Regexp.union・Regexp.compileの4行と、式展開を含むリテラル2行が出ることを確かめました。固定パターンのmatch?は出ません。
2本目は、"/users/#{id}"のような文字列のパスの組み立ても拾います。出力のうち、文字列リテラルの行は目で除外してください。拾った行ごとに、パターンへ入る値がどこから来るか(params、DB、外部API、環境変数)を記録しておくと、次の判断がしやすくなります。
Prismで構文木を走査し、Regexp呼び出しと式展開を一覧にする
grepの誤検知が多い大きなリポジトリでは、Ruby 3.3から標準で同梱されているパーサのPrismを使います。Prism::Visitorを継承し、メソッド呼び出しと式展開つきの正規表現リテラルだけを拾います。
# find_dynamic_regexp.rb:外部入力が入りうる正規表現の組み立て箇所を列挙する
require "prism"
class DynamicRegexpFinder < Prism::Visitor
TARGETS = %i[new union compile].freeze
def initialize(path, hits)
super()
@path = path
@hits = hits
end
def visit_call_node(node)
recv = node.receiver
if recv.respond_to?(:name) && recv.name == :Regexp && TARGETS.include?(node.name)
args = node.arguments&.arguments || []
# 引数がすべてリテラルなら開発者が書いたパターンとみなす
fixed = args.any? && args.all? { |a| a.is_a?(Prism::StringNode) || a.is_a?(Prism::RegularExpressionNode) }
@hits << [@path, node.location.start_line, "Regexp.#{node.name}", fixed ? "参考" : "要確認"]
end
super
end
def visit_interpolated_regular_expression_node(node)
@hits << [@path, node.location.start_line, "式展開つきリテラル", "要確認"]
super
end
end
patterns = ARGV.empty? ? ["app/**/*.rb", "lib/**/*.rb", "config/**/*.rb"] : ARGV
hits = []
Dir.glob(patterns).each do |path|
Prism.parse_file(path).value.accept(DynamicRegexpFinder.new(path, hits))
end
hits.each { |h| puts h.join("\t") }
ruby find_dynamic_regexp.rbで実行すると、ファイル名・行番号・種類・判定がタブ区切りで出ます。文字列のパス組み立ては構文木の上で別の種類になるため、grepの2本目のような誤検知は出ません。このスクリプトは公式ドキュメントのAPI名で書いたもので、私たちの環境では実行していません。使う前に数ファイルで出力を確かめてください。3.2系ではgem install prismで同じものが使えます。
継続して見張るなら、同じ判定をRuboCopの独自ルールにしてCIへ組み込む方法もあります。設定の置き場所や自動修正の扱いはRuboCopの使い方と.rubocop.yml設定を参照してください。
gemの中で正規表現を組む箇所と外部入力が届く経路の確かめ方
自社のコードに該当箇所が無くても、gemが内部で外部入力から正規表現を組んでいる可能性はあります。ruby find_dynamic_regexp.rb "$(bundle info --path gem名)/lib/**/*.rb"のようにgemのパスを渡せば、同じ走査ができます。ただしgemの中は件数が多く、すべてを読むのは現実的ではありません。
gemの洗い出しより、Ruby本体の更新を先に済ませるほうが早く確実です。gemの調査は、更新までの間に外部から届く入力の経路(検索、フィルタ、Webhookの受信、インポート機能)に絞って行います。APIの入口ごとの入力検証の考え方はAPI不正アクセス対策の実装手順で扱っています。
更新を待てない場合の書き換え:include?・許可リスト・escapeの使い分け
Rubyの更新には、ベースイメージの差し替えや回帰テストが伴います。数日かかる場合に、洗い出した箇所を当面どう書き換えるかを整理します。
部分一致はLIKEとinclude?へ、選択式の絞り込みは許可リストへ置き換える
利用者が入力した語で探すだけなら、正規表現は要りません。DBで探すならLIKE、メモリ上ならString#include?に置き換えます。
# 書き換え前:検索語をそのまま正規表現としてコンパイルしている
@items = Item.where(active: true).select { |i| i.name =~ Regexp.new(params[:q], Regexp::IGNORECASE) }
# 書き換え後1:部分一致はDBのLIKEで探す(%や_もエスケープする)
q = Item.sanitize_sql_like(params[:q].to_s)
@items = Item.where(active: true).where("name LIKE ?", "%#{q}%")
# 書き換え後2:選択肢から選ぶ絞り込みは、固定のパターンへ引き当てる
FILTERS = {
"zip" => /\A\d{3}-\d{4}\z/,
"order" => /\AORD-\d{8}\z/
}.freeze
pattern = FILTERS.fetch(params[:filter]) { raise ActionController::BadRequest }
sanitize_sql_likeは、LIKEで特別な意味を持つ%と_をエスケープするActive Recordのメソッドです。許可リスト方式では、利用者はキーを選ぶだけになり、パターンの中身には触れられません。管理画面で登録するNGワードも、正規表現ではなく語の一覧として保存し、照合はinclude?で行う形に変えられます。
Regexp.escapeで足りる場面と足りない場面の私たちの判断
Regexp.escapeは、正規表現で特別な意味を持つ文字をエスケープします。3本の修正PRの説明を読むと、引き金はどれも量指定子や繰り返しの構文です。エスケープした入力ではこれらの構文を書けないため、危険は大きく下がると私たちは考えます。
ただし、公式の告知はRegexp.escapeを対策として挙げていません。告知が示す回避策は「信頼できない入力から正規表現をコンパイルしない」だけです。そのため、エスケープを足したことを理由にRubyの更新を後回しにはしません。すでにエスケープしている箇所は、更新までの優先度を下げる目安としてだけ使います。正規表現の記号の意味と言語ごとの方言は正規表現の基本記号と言語ごとの方言で確認できます。
Ruby本体の更新を最優先し、コードの書き換えを後回しにしてよい条件
洗い出しと書き換えは、更新ができない期間の穴埋めです。次の条件を満たすなら、書き換えは後回しにして今日のうちに更新を済ませます。
- 同じ系列の修正版(4.0.9・3.4.12・3.3.13)へ上げるだけで、系列をまたがない
- CIで回帰テストが回り、ステージング環境で動作を確かめられる
- ベースイメージのタグを書き換えて再ビルドするだけで本番へ出せる
同じ系列内の更新は、正規表現エンジンの修正と細かなバグ修正が中心で、アプリの書き換えが要る変更は通常含まれません。書き換えに数日かけるより、更新を先に出すほうが露出する期間を短くできる方法です。逆に3.2系から3.3系へ移るなど系列をまたぐ場合は、依存gemの対応確認で時間がかかるため、書き換えを先に入れます。社外に公開している複数のRubyアプリをまとめて点検する場合は、脆弱性診断の種類と外注時の判断基準で進め方を確認できます。
Rubyの正規表現脆弱性の更新対応でよく受ける質問と判断のための回答
今回の更新を進める中で出てきやすい疑問に、公開されている一次情報の範囲で答えます。
Ruby on Railsのバージョンを上げれば対策になりますか?
なりません。欠陥はRuby本体に組み込まれた正規表現エンジンOnigmoにあり、Railsやgemの更新では直りません。Rubyを4.0.9・3.4.12・3.3.13以上へ上げてください。Railsの版はそのままで構いません。ただし3.2系から3.3系へ移る場合は、使っているRailsの版が移行先のRubyに対応しているかを先に確かめます。
正規表現を自分で書いているだけのアプリも更新が必要ですか?
公式は、開発者が書いた正規表現だけを使うアプリは影響を受けないとしています。急ぎの対応は不要です。ただし、自社コードに該当箇所が無くても、gemの中で外部入力から正規表現を組んでいる可能性は残ります。次の定期メンテナンスで修正版へ上げておくのが無難です。
Regexp.timeoutを設定していれば安全ですか?
安全ではありません。Regexp.timeoutはマッチに時間がかかりすぎたときに打ち切る仕組みで、今回の欠陥はその前のコンパイル段階でメモリを壊します。公式の告知も、タイムアウトでは防げないと明記しています。ReDoS対策としての設定は残してよいものの、今回の件の代わりにはなりません。
Ruby 3.2を使っている場合はどうすればよいですか?
3.2系は2026年4月1日にサポートが終わっており、修正版は出ません。3.3.13以上、できれば3.4.12か4.0.9へ移行します。移行が終わるまでは、外部入力から正規表現を組む箇所を洗い出し、LIKEやinclude?、許可リストへ置き換えてしのいでください。
脆弱性スキャナで検出されないのはなぜですか?
4件のCVEはGitLabが自社製品の欠陥として採番したもので、Ruby本体のCVEは10月31日時点で出ていません。製品名とCVEを照合するスキャナは、Rubyの実行環境を対象として扱わない可能性があります。スキャン結果に出なくても、実行中のRubyの版を直接確かめてください。
関連記事
- Rubyの最新バージョンは4.0.7|4.1.0-preview1の変更点・3系との違い・サポート期限(2026年10月):各系列の違いとサポート期限、更新の進め方です。
- GitLab脆弱性の対応手順:CVSS 10.0の修正版19.5.1/19.4.3と判断基準【2026年10月】:今回のCVEを採番したGitLab側の更新手順です。
- ReDoSとは?正規表現のバックトラック爆発を6言語7エンジンで実測し対策を判断する:タイムアウトで防ぐ計算量の問題との違いが分かります。
- 正規表現とは?基本記号の読み方と言語ごとの方言・ReDoS対策を実装目線で解説:Rubyの正規表現の書き方を見直すときの基礎です。
- API不正アクセス対策の実装手順:JPCERT注意喚起に沿うレート制限・認可・トークン失効とアプリ解析・SSRF対策:外部入力が届く入口ごとの守り方です。