`rails query`コマンドの書き込み保護バイパスと単一レコード返却のバグを修正

rails/rails

rails queryコマンドに存在した2つのバグが修正されました。1つはActiveRecord式の評価時に書き込み保護が適用されず、LLMによるdelete_all等の破壊的操作が実行可能だった脆弱性(#57420)、もう1つは単一レコードを返す式で有用な情報が得られなかった問題(#57419)です。

背景

rails queryはActiveRecord式またはRaw SQLを実行してJSON形式で結果を返すコマンドで、LLMとの統合を想定した設計ですが、その用途ゆえに2つの問題が顕在化しました。

第一の問題は、書き込み保護のバイパスです。execute_arメソッド内のeval呼び出しはwith_readonly_connectionブロックの外で実行されており、User.delete_alluser.destroyといった破壊的なActiveRecord式をガードなしで実行できる状態でした。読み取り専用ガードはActiveRecord::Relationが返ってきた後にconnection.select_allを呼ぶ経路のみに適用されており、eval自体のサイドエフェクトは保護対象外でした。Issue報告者の環境では、LLMが本番環境に対してUser.delete_allを実行し、FK制約により辛うじてデータ消失を免れたケースが報告されています。

第二の問題は、単一レコードを返す式での出力の劣化です。User.find(1)のようにActiveRecord::Baseインスタンスを返す式を実行すると、elseブランチに落ち、結果が"#<User:0x00007fb9de746518>"のようなオブジェクトのinspect文字列として返されていました。同様にUser.first(2)のようにActiveRecord::Baseインスタンスの配列を返す場合も、各要素がinspect文字列になるという問題がありました。

技術的な変更

変更はrailties/lib/rails/commands/query/query_command.rbに集中しており、書き込み保護の適用範囲の拡張と結果型の処理追加の2点で構成されます。

書き込み保護の修正として、execute_arrun_explainの両メソッドでeval呼び出し自体をwith_readonly_connectionブロックで包むよう変更されました。

変更前:

def execute_ar(expression:, page:, per:)
  result = eval(expression, TOPLEVEL_BINDING, "(query)", 1)
  # ...
end

変更後:

def execute_ar(expression:, page:, per:)
  result = with_readonly_connection { eval(expression, TOPLEVEL_BINDING, "(query)", 1) }
  # ...
end

run_explainevalも同様にwith_readonly_connectionで包まれています。これにより、評価対象の式がどのような型を返すかに関わらず、eval中に発生するすべてのDB書き込みが抑止されます。

単一レコード返却の修正として、case result文にActiveRecord::Basewhen節が追加されました。また、Arrayブランチでは先頭要素がActiveRecord::Baseのインスタンスかどうかを確認し、そうであれば各レコードのattributesを展開してカラム名と値の配列を構築するよう変更されています。

when ActiveRecord::Base
  attrs = result.attributes
  { columns: attrs.keys, rows: [ attrs.values ], sql: expression, truncated: false }
when Array
  if result.first.is_a?(ActiveRecord::Base)
    columns = result.first.attributes.keys
    rows = result.map { |record| record.attributes.values }
    { columns: columns, rows: rows, sql: expression, truncated: false }
  else
    rows = result.map { |val| Array(val) }
    cols = Array.new(rows.first&.length.to_i) { |i| "column_#{i}" }
    { columns: cols, rows: rows, sql: expression, truncated: false }
  end

テストはrailties/test/commands/query_test.rbに52行追加されており、Post.delete_allPost.first.update!explainでの書き込み防止、およびPost.firstでの単一レコード取得が正しくカラム付きで返ることを検証しています。

設計判断

eval自体をwith_readonly_connectionで包む方式が採用されています。Issue #57420ではActiveRecord::Base.while_preventing_writesを使う案も提案されていましたが、with_readonly_connectionでevalを囲む最小限の変更が選択されました。なお、マルチDB構成ではwith_readonly_connectionActiveRecord::Baseのコネクションクラスを基準にするため、異なるコネクションクラスを使うモデルへの完全な保護は別途検討が必要な点はIssueでも言及されています。

ActiveRecord::Basewhen節はActiveRecord::Relationwhen節より後に配置されており、Relationがそのままヒットしないよう型の優先順位が維持されています。Arrayブランチでのresult.first.is_a?(ActiveRecord::Base)による分岐は、既存のプリミティブ配列処理との後方互換性を保ちながらARインスタンス配列を正しく扱うための判断です。

まとめ

この修正により、rails queryがLLMから利用される際にevalの評価タイミングで書き込みが実行されるという本質的な脆弱性が解消されました。evalwith_readonly_connectionで包むという局所的な変更で根本原因に対処しつつ、単一レコードのシリアライズ欠落も同時に修正したことで、コマンドの実用性が大幅に向上しています。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
266b8674

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
リトライ後承認
Review Count:
2回 (改善を経て承認)
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

「総論→各論→結論」の構成が記事全体と各セクションで明確に適用されています。リード文、背景、技術詳細、設計判断、まとめの各要素がすべて含まれており、理想的な構成です。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライト(```ruby:path/to/file.rb)と、PR・Issueへのリンク記法([#123](URL))が正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

内容は`rails query`コマンドの内部実装に関するもので、専門用語が適切に使用されており、専門知識を持つエンジニアという対象読者に適合しています。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが「総論→各論」で構成され、各段落はトピックセンテンスで始まり、1段落1トピックの原則が守られています。段落長も適切で可読性が高いです。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事内で引用されているコードブロックは、提供されたDiffの内容と完全に一致しています。ファイル名も正確です。テストの追加行数(52行)の記述も正確でした。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

`with_readonly_connection`、`ActiveRecord::Base`、`eval`といった技術用語が文脈に沿って正確に使用されています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

書き込み保護がバイパスされる原因(evalがブロック外)や、単一レコードの結果が劣化する問題についての説明は、コードの変更内容と完全に一致しており、技術的に正確です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内のすべての主張は、PRのDescriptionおよびPRが参照するIssue(#57419, #57420)の内容に基づいています。LLMが本番環境で破壊的操作を行った事例など、Issueから文脈を補強する引用も適切です。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#57422)および関連するIssue番号(#57419, #57420)が正確に記載され、リンクされています。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトルは、PRが修正した2つの主要なバグ(書き込み保護バイパスと単一レコード返却の問題)を具体的に反映しており、PRの汎用的なタイトルよりも内容を的確に表しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

記事はPRおよび関連Issueの情報に限定されており、バージョンサポート状況やリリース日程といった外部知識の追記は見られません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

「修正されました」といった過去形の表現が使われており、マージ済みの変更を説明する記事として時間表現は正確です。