`FailureApp`の非GETタイムアウトリダイレクトにおけるオープンリダイレクト脆弱性を修正
Timeoutableモジュール有効時に、非GETリクエストでセッションタイムアウトが発生した場合、FailureApp#redirect_urlがRefererヘッダーを無検証でリダイレクト先として使用していた脆弱性(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_locationはlib/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_forがReferer由来の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に一本化され、将来の保護強化が両パスに同時に適用される一貫性のある設計となりました。