DiskService#path_for のパストラバーサル防止とエラーハンドリング強化
Rails 8.1.2.1 で導入された CVE-2026-33195 対策を、activerecord-tenanted がオーバーライドしている ActiveStorage::Service::DiskService#path_for に移植しました。キーに不正なセグメントが含まれる場合は ActiveStorage::InvalidKeyError を即座に送出し、パスがルート外へ逃げ出すことを防ぎます。さらに、ArgumentError と Encoding::CompatibilityError を同例外へ統一し、テナント構造特有の空セグメント検知も追加しました。
背景
上流 Rails が CVE-2026-33195 に対処し、DiskService#path_for にパストラバーサル検出とルート外チェックを実装しましたが、activerecord-tenanted ではテナントごとにパスを組み立てる独自ロジックが上書きされていました。そのため、上流の安全策が本gemでは適用されず、悪意あるキーがファイルシステム外へ書き込まれるリスクが残っていました。PR はこのギャップを埋め、上流の防御をテナント対応版にも反映させることが目的です。
技術的な変更
Rails バージョン要件の引き上げ: Gemfile.lock と activerecord-tenanted.gemspec の依存記述を >= 8.1.2.1 に変更し、InvalidKeyError とハードニング済み path_for が利用可能になるようにしました。
DiskService#path_for のロジック強化: 以下のコード差分で主要な防御が追加されました。
@@
- if ActiveRecord::Tenanted.connection_class && key.include?("/")
- tenant, key = key.split("/", 2)
- File.join(root, tenant, folder_for(key), key)
- else
- super
- end
+ return super unless ActiveRecord::Tenanted.connection_class && key.include?("/")
+
+ if key.split("/").intersect?(%w[. ..])
+ raise ActiveStorage::InvalidKeyError, "key has path traversal segments"
+ end
+
+ tenant, key = key.split("/", 2)
+
+ if tenant.blank? || key.blank?
+ raise ActiveStorage::InvalidKeyError, "key has a blank segment"
+ end
+
+ begin
+ path = File.expand_path(File.join(root, tenant, folder_for(key), key))
+ rescue ArgumentError
+ raise ActiveStorage::InvalidKeyError, "key is an invalid string"
+ end
+
+ unless path.start_with?(File.expand_path(root) + "/")
+ raise ActiveStorage::InvalidKeyError, "key is outside of disk service root"
+ end
+
+ path
+ rescue Encoding::CompatibilityError
+ raise ActiveStorage::InvalidKeyError, "key has incompatible encoding"
テストケースの追加: test/unit/storage_test.rb にパストラバーサル、空セグメント、NULL バイト、エンコーディング不整合など様々な攻撃ベクトルに対して InvalidKeyError が発生することを検証するテストが40行追加され、正常ケースでもルート内にパスが収まることを確認しています。
設計判断
この PR では 例外統一 と 入力検証の徹底 を選択し、InvalidKeyError へすべての不正キーを集約しました。上流 Rails が導入した例外型は、開発者が期待するエラーハンドリングを統一できるため、テナントロジックでも同様に採用するのが自然です。また、return super でテナントが無いケースは既存ロジックへ委譲し、既存利用者への互換性を保ちつつ最低限の変更に留めました。要件バージョンの引き上げは、pre‑release ソフトウェアであることを踏まえて安全に実施できると判断されています。
まとめ
activerecord-tenanted は DiskService#path_for に対し、上流 Rails のパストラバーサル防止策と例外統一を取り込み、テナント固有の空セグメント検査も追加しました。結果として、悪意あるキーからのディスク書き込みリスクが大幅に低減し、エラーハンドリングが一元化されました。Rails のバージョン要件を上げたこと以外は既存 API を壊さず、堅牢性向上のみを実現しています。