セキュリティリリースの複数脆弱性修正をmainブランチへチェリーピック

rails/rails

Railsのセキュリティリリースで対応した複数の脆弱性修正をmainブランチへ取り込むPRです。XSSインジェクション、パストラバーサル、Glob injection、DoSなど、Action Dispatch・Action View・Active Storage・Active Supportにまたがる広範な修正が含まれます。

背景

このPRは、セキュリティパッチリリース向けに作成されたコミット群を、開発ブランチであるmainへチェリーピックするものです。修正対象の脆弱性は複数のコンポーネントに及んでおり、それぞれ独立した攻撃ベクタを持ちます。

修正された脆弱性は以下の5領域にわたります:

  • Action Dispatch: 開発者向けエラーページでのXSSインジェクション
  • Action View: タグヘルパーへの空白属性名注入によるHTMLインジェクション(HackerOne #3078929
  • Active Storage (DiskService): パストラバーサルおよびGlob injection
  • Active Storage (Streaming): バイトレンジリクエストを悪用したDoS
  • Active Support: SafeBufferhtml_safe?状態の伝播バグおよび科学的記数法文字列の不正変換

各修正は互いに独立しており、それぞれ固有のコードパスで修正が完結しています。

技術的な変更

Action Dispatch: エラーページのXSS修正

開発者向けエラーページで例外メッセージをコピーボタン用の <script> タグに埋め込む際、raw ヘルパーを使っていたためXSSが可能でした。

変更前:

<script type="text/plain" id="exception-message-for-copy"><%= raw @exception_message_for_copy %></script>

変更後:

<script type="text/plain" id="exception-message-for-copy"><%= @exception_message_for_copy %></script>

raw を除去してデフォルトのHTMLエスケープを有効にすることで、x</script><script>alert(1)</script> のような例外メッセージがページ中でスクリプトとして実行されなくなります。テストでは /xss_error エンドポイントから raise "x</script><script>alert(1)</script>" を発生させ、レスポンスボディに <script>alert(1)</script> が含まれず &lt;script&gt;alert(1)&lt;/script&gt; にエスケープされることを確認しています。

Action View: 空白属性名の無視によるHTMLインジェクション修正

tag_helper.rbtag_options メソッドが、空文字列や nil をキーとする属性を受け入れていたため、"" => "/onerror=alert(1)" のような入力で不正な属性を生成できました。

変更前:

options.each_pair do |key, value|
  type = TAG_TYPES[key]
  if type == :data && value.is_a?(Hash)
    value.each_pair do |k, v|
      next if v.nil?

変更後:

options.each_pair do |key, value|
  next if key.blank?

  type = TAG_TYPES[key]
  if type == :data && value.is_a?(Hash)
    value.each_pair do |k, v|
      next if k.blank? || v.nil?

トップレベルの属性キー、data-*aria-* のすべての階層で blank? チェックを追加しています。Nokogiriを使った統合テストで、空キー付き img タグのパース結果に src 属性のみが存在することを確認しています。

Active Storage DiskService: パストラバーサルおよびGlob injection修正

DiskService#path_for を全ファイルシステム操作のセキュリティゲートウェイとして再設計し、以下の多段防御を実装しました。

def path_for(key)
  if key.blank?
    raise ActiveStorage::InvalidKeyError, "key is blank"
  end

  begin
    if key.split("/").intersect?(%w[. ..])
      raise ActiveStorage::InvalidKeyError, "key has path traversal segments"
    end
  rescue Encoding::CompatibilityError
    raise ActiveStorage::InvalidKeyError, "key has incompatible encoding"
  end

  begin
    path = File.expand_path(File.join(root, folder_for(key), key))
  rescue ArgumentError
    raise ActiveStorage::InvalidKeyError, "key is an invalid string"
  end
  # ...(rootの外側チェックが続く)
end

検証の順序は、(1)空キーの拒否、(2)./..セグメントの拒否(エンコーディングエラーも捕捉)、(3)Null byteによるArgumentErrorの捕捉、(4)File.expand_path後のrootからの逸脱チェック、という多層構造です。新たに定義した ActiveStorage::InvalidKeyError が全パターンで一貫して送出されるようになり、従来のArgumentErrorEncoding::CompatibilityErrorが漏れ出す問題も解消されます。

delete_prefixed ではGlob metacharacterのエスケープを追加しています。

escaped = escape_glob_metacharacters(prefix_path)
Dir.glob("#{escaped}*").each do |path|
  FileUtils.rm_rf(path)
end

DiskController でも showupdate の両アクションで InvalidKeyError をrescueし、それぞれ :not_found:unprocessable_content を返すようになっています。

Active Storage Streaming: バイトレンジDoS対策

HTTP Range Requestsを悪用した大量データ転送によるDoSを防ぐため、2つの制限が追加されました。

streaming_chunk_max_size(デフォルト: 100.megabytes)は、リクエストされたレンジの合計バイト数の上限です。streaming_max_ranges(デフォルト: 1)は、1リクエストに含められるレンジの最大個数です。

def ranges_valid?(ranges)
  return false if ranges.blank? || ranges.all?(&:blank?)
  ranges.sum { |range| range.end - range.begin } < ActiveStorage.streaming_chunk_max_size
end

streaming_max_ranges のデフォルトが 1 になったことで、既存の多バイトレンジを使うコードは config.active_storage.streaming_max_ranges を明示的に増やす必要があります。また、create_before_direct_upload! では filter_metadata により analyzedidentifiedcomposed というシステム管理用メタデータキーをユーザー入力から除去するようになりました。

Active Support: SafeBufferのhtml_safe?伝播バグと科学的記数法の不正変換修正

SafeBuffer#% メソッドが、unsafe状態のバッファでフォーマット操作を行った結果のバッファに @html_unsafe フラグを引き継いでいませんでした。mark_unsafe! という protected メソッドを新設し、*% の両演算子でフラグを正しく伝播させています。

protected
  def mark_unsafe!
    @html_unsafe = true
  end

number_to_delimited では、delimiter_patternnil の場合(区切り文字パターン未設定時)に手動でパーツを分割するフォールバックロジックを追加し、4桁・5桁の数値で区切りが正しく入らなかったバグを修正しています。また BigDecimal() への変換前に科学的記数法文字(deE)を含む文字列を除外し、BigDecimal("123481223d98989") が巨大な数値に変換されてしまう問題を防いでいます。

設計判断

InvalidKeyError の一元化は、DiskService固有の防御設計として注目に値します。従来は ArgumentErrorEncoding::CompatibilityError など複数の例外が混在しており、呼び出し側での網羅的なrescueが困難でした。単一の InvalidKeyError に統一することで、DiskController が明確なHTTPセマンティクスで応答できるようになっています。

path_for を「全ファイルシステム操作のセキュリティゲートウェイ」として明示的にコメントで定義し、新規メソッドが path_for または make_path_for を経由することを規約として文書化した点も、防御的設計の観点から重要な判断です。

streaming_max_ranges のデフォルトを 1 に設定した判断については、設定ガイドに「大多数のユースケースをカバーし、リトライも可能」と明記されており、破壊的変更であることを認識した上でセキュリティを優先した判断です。SafeBuffer#mark_unsafe!private ではなく protected にした点は、同一クラス階層内からのサブクラスアクセスを許容しつつ外部からの呼び出しを防ぐ、Rubyの可視性設計に沿った選択です。

まとめ

本PRは、XSS・パストラバーサル・Glob injection・DoS・SafeBufferのフラグ伝播バグという複数の独立した脆弱性を、それぞれ最小限のコード変更で修正したセキュリティリリースのチェリーピックです。特にActive StorageのDiskServiceにおける多層防御の整備とInvalidKeyErrorへの統一は、今後の安全なストレージキー取り扱いの基盤となる設計変更といえます。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
500c06a5

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

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

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

リード文(総論)、背景・技術的変更・設計判断(各論)、まとめ(結論)の3部構成が明確に適用されており、模範的な記事構成です。

カスタムMarkdown構文 ✓ PASS

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

ファイル名付きシンタックスハイライト(言語:ファイルパス)とGitHubのPRリンク記法が正しく使用されています。

対象読者への適合性 ✓ PASS

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

内容は専門的な脆弱性修正に関するもので、Railsエンジニアという対象読者に適した技術レベルと表現で記述されています。

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

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

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

Diff内容との照合 ✓ PASS

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

記事内のすべてのコード引用は、提供されたDiff情報と正確に一致しています。変更の要点を的確に抽出し、解説しています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

XSS、パストラバーサル、SafeBuffer、BigDecimalなど、関連する技術用語が正確かつ適切な文脈で使用されています。

説明の技術的正確性 ✓ PASS

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

各脆弱性の原因と修正内容に関する説明は、Diffのコード変更と論理的に整合しており、技術的に正確です。

事実の突合 ✓ PASS

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

記事内のすべての主張は、PRのタイトルやDiff内のコード、テスト、コメントで裏付けられており、ハルシネーションは検出されませんでした。

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

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

PR番号(#57054)、HackerOneレポート番号、設定値(100.megabytesなど)がPR情報と正確に一致しています。

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

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

記事のタイトルはPRのタイトル「Cherry-pick security release commits onto main branch」の内容を正確に反映しており、主題に一貫性があります。

外部知識の正確性 ✓ PASS

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

記事はPRに含まれる情報のみに基づいており、サポート状況やリリース日程といったPR外の知識の追加は見られません。

時間表現の正確性 ✓ PASS

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

変更内容を記述する際の「修正しました」「追加しています」といった時間表現は、チェリーピックされた変更を説明する文脈として正確です。