`FailureApp`の非GETタイムアウトリダイレクトにおけるオープンリダイレクト脆弱性を修正

heartcombo/devise

Timeoutableモジュール有効時に、非GETリクエストでセッションタイムアウトが発生した場合、FailureApp#redirect_urlRefererヘッダーを無検証でリダイレクト先として使用していた脆弱性(CVE-2026-40295)が修正されました。攻撃者が制御する外部URLへの誘導を防ぐため、既存のextract_path_from_locationメソッドを適用してホスト部分を除去する対応が行われています。

背景

Timeoutableモジュールを有効にしたDeviseアプリケーションでは、セッションタイムアウト後のリダイレクト先URLの決定ロジックに、GETリクエストと非GETリクエストで非対称な保護が存在していました。GETリクエストのタイムアウトではサーバーサイドで保持するattempted_pathを使用するため、外部URLが混入する余地がありません。一方、非GETリクエストのタイムアウトではrequest.referrer—すなわちHTTPリクエストのRefererヘッダー—をそのままリダイレクト先として使用しており、この値は攻撃者が完全に制御できます。

この非対称性を悪用すると、攻撃者は自サーバーに自動サブミットするクロスオリジンフォームを設置し、そこに誘導した被害者のセッションがタイムアウトしているときにフォームを送信させることで、被害者を任意の外部URLへリダイレクトさせることができます。同様のリスクはDevise自身のstore_location_forメカニズムでも認識されており、extract_path_from_locationによってホスト部分を除去する保護が既に実装されていましたが、非GETタイムアウトのパスにはその保護が適用されていませんでした。

技術的な変更

修正の核心はlib/devise/failure_app.rbの1行であり、request.referrerをそのまま返していた箇所を、既存のextract_path_from_locationメソッドでラップするように変更されています。

変更前:

path = if request.get?
  attempted_path
else
  request.referrer
end

変更後:

path = if request.get?
  attempted_path
else
  extract_path_from_location(request.referrer)
end

extract_path_from_locationlib/devise/controllers/store_location.rbに定義されており、URIをパースしてパス部分のみを抽出し、ホスト・スキームを除去します。合わせて、同メソッド内にもuri.pathが存在する場合のみパス抽出を行うガード条件が追加されました。

変更前:

def extract_path_from_location(location)
  uri = parse_uri(location)

  if uri 
    path = remove_domain_from_uri(uri)

変更後:

def extract_path_from_location(location)
  uri = parse_uri(location)

  if uri && uri.path
    path = remove_domain_from_uri(uri)

この追加条件により、javascript:alert(1)mailto:foo@example.comのようなopaque URI(パス成分を持たないURI)が渡された場合、nilが返されます。failure_app.rb側ではpath || scope_urlというフォールバックがあるため、nilが返された場合はサインインページにリダイレクトされます。

テストも拡充されており、test/integration/timeoutable_test.rbに2件のインテグレーションテストが追加されています:

  • Referer: http://evil.example/phishingを持つ非GETタイムアウトでは、ホストが除去されてパス部分の/phishingのみにリダイレクトされることを確認
  • Referer: javascript:alert(1)を持つ非GETタイムアウトでは、scope_url(サインインページ)へフォールバックすることを確認

また、test/controllers/helpers_test.rbではstore_location_forに渡す不正なロケーションのテストケースが拡充され、javascript:スキームやmailto:スキームを含むopaque URIもストアされないことが明示的に検証されるようになりました。

設計判断

新たな保護ロジックを追加するのではなく、既存のextract_path_from_locationを再利用するアプローチが採用されています。

store_location_forReferer由来のURLを安全に処理するために持っていた保護ロジックをそのままFailureAppにも適用することで、修正差分を最小化し、将来的にextract_path_from_locationのロジックが改善された場合にも両方のパスが恩恵を受ける設計になっています。uri.pathのガードをextract_path_from_location本体に追加したことで、この関数を呼び出すすべての箇所でopaque URIへの耐性が一括して強化される点も重要です。

リダイレクト先がnilになった際のscope_urlへのフォールバックは既存の挙動であり、今回の修正はそのフォールバックメカニズムを自然に活用しています。

まとめ

今回の修正は、1行の変更と既存メソッドへの防御的ガード追加という最小限の変更で、非GETタイムアウトにおけるオープンリダイレクトを閉塞したセキュリティパッチです。GETパスと非GETパスで分断していたリダイレクト先の検証ロジックがextract_path_from_locationに一本化され、将来の保護強化が両パスに同時に適用される一貫性のある設計となりました。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
916dfc9e

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

品質レビュー結果

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

Review Criteria:

記事構成 ✓ PASS

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

リード文(総論)→背景・技術詳細・設計判断(各論)→まとめ(結論)の構成が明確で、ガイドラインを完全に満たしています。

カスタムMarkdown構文 ⚠ WARNING

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

ファイル名付きシンタックスハイライトは正しく使用されています。しかし、Primary SourceのリンクでコミットIDをPR番号として表記している(`[PR #025fe21]`)ため、警告とします。

対象読者への適合性 ✓ PASS

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

Deviseの内部実装やセキュリティ脆弱性に関する専門的な内容であり、対象読者であるエンジニアに適した技術レベルと表現です。

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

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

各セクションが総論→各論で構成され、各段落がトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が徹底されており、非常に可読性が高いです。

Diff内容との照合 ✓ PASS

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

記事内のコード引用は、提供されたDiffの内容と完全に一致しており、ファイル名も正確です。テストコードの変更点に関する説明も的確です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「Timeoutable」「FailureApp」「opaque URI」などの専門用語が文脈に応じて正確に使用されています。

説明の技術的正確性 ✓ PASS

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

`extract_path_from_location`の役割や、`uri.path`のガード条件がもたらす影響(opaque URIの処理)についての説明は、技術的に正確で論理的です。

事実の突合 ✓ PASS

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

記事内の主張はすべてPRのDescriptionやDiffのコード変更で裏付けられています。「設計判断」セクションはコードの意図を分析したものであり、根拠のない創作(ハルシネーション)ではありません。

数値・固有名詞の確認 ⚠ WARNING

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

CVE番号はDiff情報と一致していますが、コミットID `025fe21` を「PR #025fe21」と表記しており、固有名詞の種別が不正確です。

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

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

記事のタイトルは、PRが修正しようとしている「`FailureApp`におけるオープンリダイレクト脆弱性」という主題を正確に反映しています。

外部知識の正確性 ✓ PASS

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

記事で言及されているCVE番号はDiff内のCHANGELOG.mdに記載されており、PR情報に基づかない外部知識の捏造は見られません。

時間表現の正確性 ✓ PASS

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

記事内の時間表現はPR情報と矛盾しておらず、正確です。