HostAuthorization が余分なポートを許可しないように修正
HostAuthorization が明示的にポートを指定したホストに対して余分なオプションポート正規表現を付与し、www.example.com:80:80 のような不正な Host が許可されていた問題を解消します。これにより、リダイレクト処理が期待通りに動作し、サーバーエラーを防げます。
背景
問題の根源は action_dispatch/middleware/host_authorization.rb の Permissions#sanitize_string が常に PORT_REGEX? を付与した点にあります。許可リストに www.example.com:80 が含まれていても、正規表現は任意のポートを許容するため www.example.com:80:80 がマッチし、内部リダイレクトで不正なホストが処理されました。#37956 で報告されたように、Malformed Host ヘッダーが原因で 500 エラーが発生していました。
技術的な変更
sanitize_string の生成ロジックを port_regexp に置き換え、ホストに既にポートが含まれる場合は空文字列を返すようにしました。
@@
- /\A#{Regexp.escape host}#{PORT_REGEX}?\z/i
+ /\A#{Regexp.escape host}#{port_regexp(host)}\z/i
新たに導入した port_regexp と host_with_port? は以下の通りです。
@@
- def sanitize_string(host)
- if host.start_with?(".")
- /\A#{SUBDOMAIN_REGEX}?#{Regexp.escape(host[1..-1])}#{PORT_REGEX}?\z/i
- else
- /\A#{Regexp.escape host}#{PORT_REGEX}?\z/i
- end
- end
+ def sanitize_string(host)
+ if host.start_with?(".")
+ /\A#{SUBDOMAIN_REGEX}?#{Regexp.escape(host[1..-1])}#{port_regexp(host)}\z/i
+ else
+ /\A#{Regexp.escape host}#{port_regexp(host)}\z/i
+ end
+ end
+
+ def port_regexp(host)
+ host_with_port?(host) ? "" : "#{PORT_REGEX}?"
+ end
+
+ def host_with_port?(host)
+ if host.start_with?("[")
+ host.match?(/\]:\d+\z/)
+ else
+ host.count(":") == 1 && host.match?(/:\d+\z/)
+ end
+ end
テストも拡充し、ポート付きホストが正常に動作するケースと、余分なポートを持つホストが Forbidden になるケースを検証しています。
@@
test "hosts configured with explicit port work" do
@@
test "blocks malformed hosts with extra ports" do
@@
assert_response :forbidden
assert_match "Blocked hosts: www.example.com:80:80", response.body
end
設計判断
後方互換性の維持を最優先し、既存の「ポートが無いホストは任意ポートを許容」ロジックはそのままにしました。port_regexp が空文字列を返す条件を host_with_port? で判定するだけなので、既存コードへの侵入は最小限です。新しいヘルパーメソッドはプライベート領域に収められ、外部 API には影響しません。
このアプローチは正規表現の複雑化を避け、ホスト文字列の構造分析に委ねることで保守性を高めました。結果として、明示的ポート指定の正当性はそのまま保たれ、余分なポートが付与されたケースだけが除外されます。
まとめ
本 PR は HostAuthorization の正規表現生成を改良し、余分なポートを持つ malformed Host をブロックします。オプションポートの挙動はポート未指定時に限定され、既存設定との互換性を維持しつつ安全性が向上しました。