Skip to content

Commit b667e48

Browse files
Merge branch 'main' into button-to-authenticity-token
2 parents 71fb8eb + e0076e4 commit b667e48

File tree

88 files changed

+1169
-361
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

88 files changed

+1169
-361
lines changed

actioncable/actioncable.gemspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Gem::Specification.new do |s|
1717
1818
s.homepage = "https://rubyonrails.org"
1919

20-
s.files = Dir["CHANGELOG.md", "MIT-LICENSE", "README.md", "lib/**/*", "app/assets/javascripts/action_cable.js"]
20+
s.files = Dir["CHANGELOG.md", "MIT-LICENSE", "README.md", "lib/**/*", "app/assets/javascripts/*.js"]
2121
s.require_path = "lib"
2222

2323
s.metadata = {

actionmailer/lib/action_mailer/base.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,9 @@ module ActionMailer
435435
# * <tt>deliveries</tt> - Keeps an array of all the emails sent out through the Action Mailer with
436436
# <tt>delivery_method :test</tt>. Most useful for unit and functional testing.
437437
#
438+
# * <tt>delivery_job</tt> - The job class used with <tt>deliver_later</tt>. Defaults to
439+
# +ActionMailer::MailDeliveryJob+.
440+
#
438441
# * <tt>deliver_later_queue_name</tt> - The name of the queue used with <tt>deliver_later</tt>.
439442
class Base < AbstractController::Base
440443
include DeliveryMethods

actionpack/CHANGELOG.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,29 @@
1+
* Raise `ActionController::Redirecting::UnsafeRedirectError` for unsafe `redirect_to` redirects.
2+
3+
This allows `rescue_from` to be used to add a default fallback route:
4+
5+
```ruby
6+
rescue_from ActionController::Redirecting::UnsafeRedirectError do
7+
redirect_to root_url
8+
end
9+
```
10+
11+
*Kasper Timm Hansen*, *Chris Oliver*
12+
13+
* Add `url_from` to verify a redirect location is internal.
14+
15+
Takes the open redirect protection from `redirect_to` so users can wrap a
16+
param, and fall back to an alternate redirect URL when the param provided
17+
one is unsafe.
18+
19+
```ruby
20+
def create
21+
redirect_to url_from(params[:redirect_url]) || root_url
22+
end
23+
```
24+
25+
*dmcge*, *Kasper Timm Hansen*
26+
127
* Allow Capybara driver name overrides in `SystemTestCase::driven_by`
228

329
Allow users to prevent conflicts among drivers that use the same driver

actionpack/lib/abstract_controller/base.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ def process(action, *args)
148148

149149
@_response_body = nil
150150

151+
ActiveSupport::ExecutionContext[:controller] = self
151152
process_action(action_name, *args)
152153
end
153154

actionpack/lib/action_controller.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ module ActionController
3737
autoload :Logging
3838
autoload :MimeResponds
3939
autoload :ParamsWrapper
40-
autoload :QueryTags
4140
autoload :Redirecting
4241
autoload :Renderers
4342
autoload :Rendering

actionpack/lib/action_controller/metal/query_tags.rb

Lines changed: 0 additions & 16 deletions
This file was deleted.

actionpack/lib/action_controller/metal/redirecting.rb

Lines changed: 58 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ module Redirecting
77
include AbstractController::Logger
88
include ActionController::UrlFor
99

10+
class UnsafeRedirectError < StandardError; end
11+
1012
included do
1113
mattr_accessor :raise_on_open_redirects, default: false
1214
end
@@ -61,20 +63,34 @@ module Redirecting
6163
#
6264
# redirect_to post_url(@post) and return
6365
#
64-
# Passing user input directly into +redirect_to+ is considered dangerous (e.g. `redirect_to(params[:location])`).
65-
# Always use regular expressions or a permitted list when redirecting to a user specified location.
66+
# === Open Redirect protection
67+
#
68+
# By default, Rails protects against redirecting to external hosts for your app's safety, so called open redirects.
69+
# Note: this was a new default in Rails 7.0, after upgrading opt-in by uncommenting the line with +raise_on_open_redirects+ in <tt>config/initializers/new_framework_defaults_7_0.rb</tt>
70+
#
71+
# Here #redirect_to automatically validates the potentially-unsafe URL:
72+
#
73+
# redirect_to params[:redirect_url]
74+
#
75+
# Raises UnsafeRedirectError in the case of an unsafe redirect.
76+
#
77+
# To allow any external redirects pass `allow_other_host: true`, though using a user-provided param in that case is unsafe.
78+
#
79+
# redirect_to "https://rubyonrails.org", allow_other_host: true
80+
#
81+
# See #url_from for more information on what an internal and safe URL is, or how to fall back to an alternate redirect URL in the unsafe case.
6682
def redirect_to(options = {}, response_options = {})
67-
response_options[:allow_other_host] ||= _allow_other_host unless response_options.key?(:allow_other_host)
68-
6983
raise ActionControllerError.new("Cannot redirect to nil!") unless options
7084
raise AbstractController::DoubleRenderError if response_body
7185

86+
allow_other_host = response_options.delete(:allow_other_host) { _allow_other_host }
87+
7288
self.status = _extract_redirect_to_status(options, response_options)
73-
self.location = _compute_safe_redirect_to_location(request, options, response_options)
89+
self.location = _enforce_open_redirect_protection(_compute_redirect_to_location(request, options), allow_other_host: allow_other_host)
7490
self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(response.location)}\">redirected</a>.</body></html>"
7591
end
7692

77-
# Soft deprecated alias for <tt>redirect_back_or_to</tt> where the fallback_location location is supplied as a keyword argument instead
93+
# Soft deprecated alias for #redirect_back_or_to where the +fallback_location+ location is supplied as a keyword argument instead
7894
# of the first positional argument.
7995
def redirect_back(fallback_location:, allow_other_host: _allow_other_host, **args)
8096
redirect_back_or_to fallback_location, allow_other_host: allow_other_host, **args
@@ -103,23 +119,11 @@ def redirect_back(fallback_location:, allow_other_host: _allow_other_host, **arg
103119
# All other options that can be passed to #redirect_to are accepted as
104120
# options and the behavior is identical.
105121
def redirect_back_or_to(fallback_location, allow_other_host: _allow_other_host, **options)
106-
location = request.referer || fallback_location
107-
location = fallback_location unless allow_other_host || _url_host_allowed?(request.referer)
108-
allow_other_host = true if _allow_other_host && !allow_other_host # if the fallback is an open redirect
109-
110-
redirect_to location, allow_other_host: allow_other_host, **options
111-
end
112-
113-
def _compute_safe_redirect_to_location(request, options, response_options)
114-
location = _compute_redirect_to_location(request, options)
115-
116-
if response_options[:allow_other_host] || _url_host_allowed?(location)
117-
location
122+
if request.referer && (allow_other_host || _url_host_allowed?(request.referer))
123+
redirect_to request.referer, allow_other_host: allow_other_host, **options
118124
else
119-
raise(ArgumentError, <<~MSG.squish)
120-
Unsafe redirect #{location.truncate(100).inspect},
121-
use :allow_other_host to redirect anyway.
122-
MSG
125+
# The method level `allow_other_host` doesn't apply in the fallback case, omit and let the `redirect_to` handling take over.
126+
redirect_to fallback_location, **options
123127
end
124128
end
125129

@@ -143,6 +147,30 @@ def _compute_redirect_to_location(request, options) # :nodoc:
143147
module_function :_compute_redirect_to_location
144148
public :_compute_redirect_to_location
145149

150+
# Verifies the passed +location+ is an internal URL that's safe to redirect to and returns it, or nil if not.
151+
# Useful to wrap a params provided redirect URL and fallback to an alternate URL to redirect to:
152+
#
153+
# redirect_to url_from(params[:redirect_url]) || root_url
154+
#
155+
# The +location+ is considered internal, and safe, if it's on the same host as <tt>request.host</tt>:
156+
#
157+
# # If request.host is example.com:
158+
# url_from("https://example.com/profile") # => "https://example.com/profile"
159+
# url_from("http://example.com/profile") # => "http://example.com/profile"
160+
# url_from("http://evil.com/profile") # => nil
161+
#
162+
# Subdomains are considered part of the host:
163+
#
164+
# # If request.host is on https://example.com or https://app.example.com, you'd get:
165+
# url_from("https://dev.example.com/profile") # => nil
166+
#
167+
# NOTE: there's a similarity with {url_for}[rdoc-ref:ActionDispatch::Routing::UrlFor#url_for], which generates an internal URL from various options from within the app, e.g. <tt>url_for(@post)</tt>.
168+
# However, #url_from is meant to take an external parameter to verify as in <tt>url_from(params[:redirect_url])</tt>.
169+
def url_from(location)
170+
location = location.presence
171+
location if location && _url_host_allowed?(location)
172+
end
173+
146174
private
147175
def _allow_other_host
148176
!raise_on_open_redirects
@@ -158,6 +186,14 @@ def _extract_redirect_to_status(options, response_options)
158186
end
159187
end
160188

189+
def _enforce_open_redirect_protection(location, allow_other_host:)
190+
if allow_other_host || _url_host_allowed?(location)
191+
location
192+
else
193+
raise UnsafeRedirectError, "Unsafe redirect to #{location.truncate(100).inspect}, pass allow_other_host: true to redirect anyway."
194+
end
195+
end
196+
161197
def _url_host_allowed?(url)
162198
URI(url.to_s).host == request.host
163199
rescue ArgumentError, URI::Error

actionpack/lib/action_controller/railtie.rb

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -113,10 +113,6 @@ class Railtie < Rails::Railtie # :nodoc:
113113
if query_logs_tags_enabled
114114
app.config.active_record.query_log_tags += [:controller, :action]
115115

116-
ActiveSupport.on_load(:action_controller) do
117-
include ActionController::QueryTags
118-
end
119-
120116
ActiveSupport.on_load(:active_record) do
121117
ActiveRecord::QueryLogs.taggings.merge!(
122118
controller: ->(context) { context[:controller]&.controller_name },

actionpack/test/controller/redirect_test.rb

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ def unsafe_redirect_back
8888
redirect_back_or_to "http://www.rubyonrails.org/"
8989
end
9090

91+
def safe_redirect_with_fallback
92+
redirect_to url_from(params[:redirect_url]) || "/fallback"
93+
end
94+
9195
def redirect_back_with_explicit_fallback_kwarg
9296
redirect_back(fallback_location: "/things/stuff", status: 307)
9397
end
@@ -478,27 +482,41 @@ def test_redirect_to_with_block_and_accepted_options
478482

479483
def test_unsafe_redirect
480484
with_raise_on_open_redirects do
481-
error = assert_raise(ArgumentError) do
485+
error = assert_raise(ActionController::Redirecting::UnsafeRedirectError) do
482486
get :unsafe_redirect
483487
end
484488

485-
assert_equal(<<~MSG.squish, error.message)
486-
Unsafe redirect \"http://www.rubyonrails.org/\",
487-
use :allow_other_host to redirect anyway.
488-
MSG
489+
assert_equal "Unsafe redirect to \"http://www.rubyonrails.org/\", pass allow_other_host: true to redirect anyway.", error.message
489490
end
490491
end
491492

492493
def test_unsafe_redirect_back
493494
with_raise_on_open_redirects do
494-
error = assert_raise(ArgumentError) do
495+
error = assert_raise(ActionController::Redirecting::UnsafeRedirectError) do
495496
get :unsafe_redirect_back
496497
end
497498

498-
assert_equal(<<~MSG.squish, error.message)
499-
Unsafe redirect \"http://www.rubyonrails.org/\",
500-
use :allow_other_host to redirect anyway.
501-
MSG
499+
assert_equal "Unsafe redirect to \"http://www.rubyonrails.org/\", pass allow_other_host: true to redirect anyway.", error.message
500+
end
501+
end
502+
503+
def test_url_from
504+
with_raise_on_open_redirects do
505+
get :safe_redirect_with_fallback, params: { redirect_url: "http://test.host/app" }
506+
assert_response :redirect
507+
assert_redirected_to "http://test.host/app"
508+
end
509+
end
510+
511+
def test_url_from_fallback
512+
with_raise_on_open_redirects do
513+
get :safe_redirect_with_fallback, params: { redirect_url: "http://www.rubyonrails.org/" }
514+
assert_response :redirect
515+
assert_redirected_to "http://test.host/fallback"
516+
517+
get :safe_redirect_with_fallback, params: { redirect_url: "" }
518+
assert_response :redirect
519+
assert_redirected_to "http://test.host/fallback"
502520
end
503521
end
504522

actionview/CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,15 @@
1313

1414
*Sean Doyle*
1515

16+
* Support rendering `<form>` elements _without_ `[action]` attributes by:
17+
18+
* `form_with url: false` or `form_with ..., html: { action: false }`
19+
* `form_for ..., url: false` or `form_for ..., html: { action: false }`
20+
* `form_tag false` or `form_tag ..., action: false`
21+
* `button_to "...", false` or `button_to(false) { ... }`
22+
23+
*Sean Doyle*
24+
1625
* Add `:day_format` option to `date_select`
1726

1827
date_select("article", "written_on", day_format: ->(day) { day.ordinalize })

0 commit comments

Comments
 (0)