actionpack 7.1.5.1 → 8.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +308 -523
  3. data/README.rdoc +1 -1
  4. data/lib/abstract_controller/asset_paths.rb +6 -2
  5. data/lib/abstract_controller/base.rb +104 -105
  6. data/lib/abstract_controller/caching/fragments.rb +50 -53
  7. data/lib/abstract_controller/caching.rb +8 -3
  8. data/lib/abstract_controller/callbacks.rb +70 -62
  9. data/lib/abstract_controller/collector.rb +7 -7
  10. data/lib/abstract_controller/deprecator.rb +2 -0
  11. data/lib/abstract_controller/error.rb +2 -0
  12. data/lib/abstract_controller/helpers.rb +71 -84
  13. data/lib/abstract_controller/logger.rb +4 -1
  14. data/lib/abstract_controller/railties/routes_helpers.rb +2 -0
  15. data/lib/abstract_controller/rendering.rb +13 -13
  16. data/lib/abstract_controller/translation.rb +12 -13
  17. data/lib/abstract_controller/url_for.rb +8 -6
  18. data/lib/abstract_controller.rb +2 -0
  19. data/lib/action_controller/api/api_rendering.rb +2 -0
  20. data/lib/action_controller/api.rb +76 -72
  21. data/lib/action_controller/base.rb +199 -126
  22. data/lib/action_controller/caching.rb +16 -14
  23. data/lib/action_controller/deprecator.rb +2 -0
  24. data/lib/action_controller/form_builder.rb +21 -18
  25. data/lib/action_controller/log_subscriber.rb +23 -2
  26. data/lib/action_controller/metal/allow_browser.rb +133 -0
  27. data/lib/action_controller/metal/basic_implicit_render.rb +2 -0
  28. data/lib/action_controller/metal/conditional_get.rb +217 -175
  29. data/lib/action_controller/metal/content_security_policy.rb +25 -24
  30. data/lib/action_controller/metal/cookies.rb +4 -2
  31. data/lib/action_controller/metal/data_streaming.rb +72 -63
  32. data/lib/action_controller/metal/default_headers.rb +5 -3
  33. data/lib/action_controller/metal/etag_with_flash.rb +3 -1
  34. data/lib/action_controller/metal/etag_with_template_digest.rb +17 -15
  35. data/lib/action_controller/metal/exceptions.rb +16 -9
  36. data/lib/action_controller/metal/flash.rb +13 -14
  37. data/lib/action_controller/metal/head.rb +15 -11
  38. data/lib/action_controller/metal/helpers.rb +63 -55
  39. data/lib/action_controller/metal/http_authentication.rb +209 -201
  40. data/lib/action_controller/metal/implicit_render.rb +17 -15
  41. data/lib/action_controller/metal/instrumentation.rb +16 -14
  42. data/lib/action_controller/metal/live.rb +177 -128
  43. data/lib/action_controller/metal/logging.rb +6 -4
  44. data/lib/action_controller/metal/mime_responds.rb +151 -142
  45. data/lib/action_controller/metal/parameter_encoding.rb +34 -32
  46. data/lib/action_controller/metal/params_wrapper.rb +57 -59
  47. data/lib/action_controller/metal/permissions_policy.rb +22 -12
  48. data/lib/action_controller/metal/rate_limiting.rb +92 -0
  49. data/lib/action_controller/metal/redirecting.rb +213 -94
  50. data/lib/action_controller/metal/renderers.rb +78 -57
  51. data/lib/action_controller/metal/rendering.rb +111 -77
  52. data/lib/action_controller/metal/request_forgery_protection.rb +182 -143
  53. data/lib/action_controller/metal/rescue.rb +20 -9
  54. data/lib/action_controller/metal/streaming.rb +118 -195
  55. data/lib/action_controller/metal/strong_parameters.rb +720 -530
  56. data/lib/action_controller/metal/testing.rb +2 -0
  57. data/lib/action_controller/metal/url_for.rb +17 -15
  58. data/lib/action_controller/metal.rb +86 -60
  59. data/lib/action_controller/railtie.rb +36 -15
  60. data/lib/action_controller/railties/helpers.rb +2 -0
  61. data/lib/action_controller/renderer.rb +41 -36
  62. data/lib/action_controller/structured_event_subscriber.rb +116 -0
  63. data/lib/action_controller/template_assertions.rb +4 -2
  64. data/lib/action_controller/test_case.rb +160 -131
  65. data/lib/action_controller.rb +5 -1
  66. data/lib/action_dispatch/constants.rb +8 -0
  67. data/lib/action_dispatch/deprecator.rb +2 -0
  68. data/lib/action_dispatch/http/cache.rb +163 -35
  69. data/lib/action_dispatch/http/content_disposition.rb +2 -0
  70. data/lib/action_dispatch/http/content_security_policy.rb +54 -39
  71. data/lib/action_dispatch/http/filter_parameters.rb +14 -8
  72. data/lib/action_dispatch/http/filter_redirect.rb +22 -1
  73. data/lib/action_dispatch/http/headers.rb +22 -22
  74. data/lib/action_dispatch/http/mime_negotiation.rb +89 -41
  75. data/lib/action_dispatch/http/mime_type.rb +25 -21
  76. data/lib/action_dispatch/http/mime_types.rb +3 -0
  77. data/lib/action_dispatch/http/param_builder.rb +187 -0
  78. data/lib/action_dispatch/http/param_error.rb +26 -0
  79. data/lib/action_dispatch/http/parameters.rb +14 -12
  80. data/lib/action_dispatch/http/permissions_policy.rb +25 -36
  81. data/lib/action_dispatch/http/query_parser.rb +55 -0
  82. data/lib/action_dispatch/http/rack_cache.rb +2 -0
  83. data/lib/action_dispatch/http/request.rb +141 -92
  84. data/lib/action_dispatch/http/response.rb +137 -77
  85. data/lib/action_dispatch/http/upload.rb +18 -16
  86. data/lib/action_dispatch/http/url.rb +187 -89
  87. data/lib/action_dispatch/journey/formatter.rb +21 -9
  88. data/lib/action_dispatch/journey/gtg/builder.rb +4 -3
  89. data/lib/action_dispatch/journey/gtg/simulator.rb +34 -11
  90. data/lib/action_dispatch/journey/gtg/transition_table.rb +47 -53
  91. data/lib/action_dispatch/journey/nfa/dot.rb +2 -0
  92. data/lib/action_dispatch/journey/nodes/node.rb +8 -6
  93. data/lib/action_dispatch/journey/parser.rb +99 -195
  94. data/lib/action_dispatch/journey/path/pattern.rb +4 -1
  95. data/lib/action_dispatch/journey/route.rb +54 -38
  96. data/lib/action_dispatch/journey/router/utils.rb +22 -27
  97. data/lib/action_dispatch/journey/router.rb +63 -83
  98. data/lib/action_dispatch/journey/routes.rb +11 -2
  99. data/lib/action_dispatch/journey/scanner.rb +46 -42
  100. data/lib/action_dispatch/journey/visitors.rb +57 -23
  101. data/lib/action_dispatch/journey/visualizer/fsm.js +4 -6
  102. data/lib/action_dispatch/journey.rb +2 -0
  103. data/lib/action_dispatch/log_subscriber.rb +7 -1
  104. data/lib/action_dispatch/middleware/actionable_exceptions.rb +2 -0
  105. data/lib/action_dispatch/middleware/assume_ssl.rb +8 -5
  106. data/lib/action_dispatch/middleware/callbacks.rb +3 -1
  107. data/lib/action_dispatch/middleware/cookies.rb +125 -106
  108. data/lib/action_dispatch/middleware/debug_exceptions.rb +37 -8
  109. data/lib/action_dispatch/middleware/debug_locks.rb +15 -13
  110. data/lib/action_dispatch/middleware/debug_view.rb +13 -5
  111. data/lib/action_dispatch/middleware/exception_wrapper.rb +18 -23
  112. data/lib/action_dispatch/middleware/executor.rb +19 -4
  113. data/lib/action_dispatch/middleware/flash.rb +63 -51
  114. data/lib/action_dispatch/middleware/host_authorization.rb +17 -15
  115. data/lib/action_dispatch/middleware/public_exceptions.rb +14 -12
  116. data/lib/action_dispatch/middleware/reloader.rb +5 -3
  117. data/lib/action_dispatch/middleware/remote_ip.rb +87 -77
  118. data/lib/action_dispatch/middleware/request_id.rb +16 -10
  119. data/lib/action_dispatch/middleware/server_timing.rb +4 -2
  120. data/lib/action_dispatch/middleware/session/abstract_store.rb +2 -0
  121. data/lib/action_dispatch/middleware/session/cache_store.rb +30 -8
  122. data/lib/action_dispatch/middleware/session/cookie_store.rb +27 -26
  123. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +7 -3
  124. data/lib/action_dispatch/middleware/show_exceptions.rb +16 -16
  125. data/lib/action_dispatch/middleware/ssl.rb +53 -40
  126. data/lib/action_dispatch/middleware/stack.rb +11 -10
  127. data/lib/action_dispatch/middleware/static.rb +33 -31
  128. data/lib/action_dispatch/middleware/templates/rescues/_copy_button.html.erb +1 -0
  129. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +3 -5
  130. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +9 -5
  131. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +1 -0
  132. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +1 -0
  133. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +4 -0
  134. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +3 -0
  135. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +50 -0
  136. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +1 -0
  137. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +1 -0
  138. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +1 -0
  139. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +1 -0
  140. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +1 -0
  141. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +1 -1
  142. data/lib/action_dispatch/railtie.rb +23 -3
  143. data/lib/action_dispatch/request/session.rb +24 -21
  144. data/lib/action_dispatch/request/utils.rb +11 -3
  145. data/lib/action_dispatch/routing/endpoint.rb +2 -0
  146. data/lib/action_dispatch/routing/inspector.rb +85 -60
  147. data/lib/action_dispatch/routing/mapper.rb +1031 -851
  148. data/lib/action_dispatch/routing/polymorphic_routes.rb +69 -62
  149. data/lib/action_dispatch/routing/redirection.rb +47 -39
  150. data/lib/action_dispatch/routing/route_set.rb +79 -56
  151. data/lib/action_dispatch/routing/routes_proxy.rb +7 -4
  152. data/lib/action_dispatch/routing/url_for.rb +130 -125
  153. data/lib/action_dispatch/routing.rb +150 -148
  154. data/lib/action_dispatch/structured_event_subscriber.rb +20 -0
  155. data/lib/action_dispatch/system_test_case.rb +91 -81
  156. data/lib/action_dispatch/system_testing/browser.rb +16 -23
  157. data/lib/action_dispatch/system_testing/driver.rb +2 -0
  158. data/lib/action_dispatch/system_testing/server.rb +2 -0
  159. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +34 -23
  160. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +2 -0
  161. data/lib/action_dispatch/testing/assertion_response.rb +9 -7
  162. data/lib/action_dispatch/testing/assertions/response.rb +52 -25
  163. data/lib/action_dispatch/testing/assertions/routing.rb +168 -87
  164. data/lib/action_dispatch/testing/assertions.rb +2 -0
  165. data/lib/action_dispatch/testing/integration.rb +233 -223
  166. data/lib/action_dispatch/testing/request_encoder.rb +11 -9
  167. data/lib/action_dispatch/testing/test_helpers/page_dump_helper.rb +35 -0
  168. data/lib/action_dispatch/testing/test_process.rb +11 -8
  169. data/lib/action_dispatch/testing/test_request.rb +3 -1
  170. data/lib/action_dispatch/testing/test_response.rb +27 -26
  171. data/lib/action_dispatch.rb +36 -32
  172. data/lib/action_pack/gem_version.rb +6 -4
  173. data/lib/action_pack/version.rb +3 -1
  174. data/lib/action_pack.rb +17 -16
  175. metadata +36 -32
  176. data/lib/action_dispatch/journey/parser.y +0 -50
  177. data/lib/action_dispatch/journey/parser_extras.rb +0 -31
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # :markup: markdown
4
+
3
5
  module ActionController
4
6
  module Redirecting
5
7
  extend ActiveSupport::Concern
@@ -9,140 +11,212 @@ module ActionController
9
11
 
10
12
  class UnsafeRedirectError < StandardError; end
11
13
 
12
- ILLEGAL_HEADER_VALUE_REGEX = /[\x00-\x08\x0A-\x1F]/.freeze
14
+ class OpenRedirectError < UnsafeRedirectError
15
+ def initialize(location)
16
+ super("Unsafe redirect to #{location.to_s.truncate(100).inspect}, pass allow_other_host: true to redirect anyway.")
17
+ end
18
+ end
19
+
20
+ class PathRelativeRedirectError < UnsafeRedirectError
21
+ def initialize(url)
22
+ super("Path relative URL redirect detected: #{url.inspect}")
23
+ end
24
+ end
25
+
26
+ ILLEGAL_HEADER_VALUE_REGEX = /[\x00-\x08\x0A-\x1F]/
13
27
 
14
28
  included do
15
29
  mattr_accessor :raise_on_open_redirects, default: false
30
+ mattr_accessor :action_on_open_redirect, default: :log
31
+ mattr_accessor :action_on_path_relative_redirect, default: :log
32
+ class_attribute :_allowed_redirect_hosts, :allowed_redirect_hosts_permissions, instance_accessor: false, instance_predicate: false
33
+ singleton_class.alias_method :allowed_redirect_hosts, :_allowed_redirect_hosts
16
34
  end
17
35
 
18
- # Redirects the browser to the target specified in +options+. This parameter can be any one of:
36
+ module ClassMethods # :nodoc:
37
+ def allowed_redirect_hosts=(hosts)
38
+ hosts = hosts.dup.freeze
39
+ self._allowed_redirect_hosts = hosts
40
+ self.allowed_redirect_hosts_permissions = if hosts.present?
41
+ ActionDispatch::HostAuthorization::Permissions.new(hosts)
42
+ end
43
+ end
44
+ end
45
+
46
+ # Redirects the browser to the target specified in `options`. This parameter can
47
+ # be any one of:
48
+ #
49
+ # * `Hash` - The URL will be generated by calling url_for with the `options`.
50
+ # * `Record` - The URL will be generated by calling url_for with the
51
+ # `options`, which will reference a named URL for that record.
52
+ # * `String` starting with `protocol://` (like `http://`) or a protocol
53
+ # relative reference (like `//`) - Is passed straight through as the target
54
+ # for redirection.
55
+ # * `String` not containing a protocol - The current protocol and host is
56
+ # prepended to the string.
57
+ # * `Proc` - A block that will be executed in the controller's context. Should
58
+ # return any option accepted by `redirect_to`.
19
59
  #
20
- # * <tt>Hash</tt> - The URL will be generated by calling url_for with the +options+.
21
- # * <tt>Record</tt> - The URL will be generated by calling url_for with the +options+, which will reference a named URL for that record.
22
- # * <tt>String</tt> starting with <tt>protocol://</tt> (like <tt>http://</tt>) or a protocol relative reference (like <tt>//</tt>) - Is passed straight through as the target for redirection.
23
- # * <tt>String</tt> not containing a protocol - The current protocol and host is prepended to the string.
24
- # * <tt>Proc</tt> - A block that will be executed in the controller's context. Should return any option accepted by +redirect_to+.
25
60
  #
26
- # === Examples
61
+ # ### Examples
27
62
  #
28
- # redirect_to action: "show", id: 5
29
- # redirect_to @post
30
- # redirect_to "http://www.rubyonrails.org"
31
- # redirect_to "/images/screenshot.jpg"
32
- # redirect_to posts_url
33
- # redirect_to proc { edit_post_url(@post) }
63
+ # redirect_to action: "show", id: 5
64
+ # redirect_to @post
65
+ # redirect_to "http://www.rubyonrails.org"
66
+ # redirect_to "/images/screenshot.jpg"
67
+ # redirect_to posts_url
68
+ # redirect_to proc { edit_post_url(@post) }
34
69
  #
35
- # The redirection happens as a <tt>302 Found</tt> header unless otherwise specified using the <tt>:status</tt> option:
70
+ # The redirection happens as a `302 Found` header unless otherwise specified
71
+ # using the `:status` option:
36
72
  #
37
- # redirect_to post_url(@post), status: :found
38
- # redirect_to action: 'atom', status: :moved_permanently
39
- # redirect_to post_url(@post), status: 301
40
- # redirect_to action: 'atom', status: 302
73
+ # redirect_to post_url(@post), status: :found
74
+ # redirect_to action: 'atom', status: :moved_permanently
75
+ # redirect_to post_url(@post), status: 301
76
+ # redirect_to action: 'atom', status: 302
41
77
  #
42
- # The status code can either be a standard {HTTP Status code}[https://www.iana.org/assignments/http-status-codes] as an
43
- # integer, or a symbol representing the downcased, underscored and symbolized description.
44
- # Note that the status code must be a 3xx HTTP code, or redirection will not occur.
78
+ # The status code can either be a standard [HTTP Status
79
+ # code](https://www.iana.org/assignments/http-status-codes) as an integer, or a
80
+ # symbol representing the downcased, underscored and symbolized description.
81
+ # Note that the status code must be a 3xx HTTP code, or redirection will not
82
+ # occur.
45
83
  #
46
84
  # If you are using XHR requests other than GET or POST and redirecting after the
47
85
  # request then some browsers will follow the redirect using the original request
48
86
  # method. This may lead to undesirable behavior such as a double DELETE. To work
49
- # around this you can return a <tt>303 See Other</tt> status code which will be
87
+ # around this you can return a `303 See Other` status code which will be
50
88
  # followed using a GET request.
51
89
  #
52
- # redirect_to posts_url, status: :see_other
53
- # redirect_to action: 'index', status: 303
90
+ # redirect_to posts_url, status: :see_other
91
+ # redirect_to action: 'index', status: 303
54
92
  #
55
- # It is also possible to assign a flash message as part of the redirection. There are two special accessors for the commonly used flash names
56
- # +alert+ and +notice+ as well as a general purpose +flash+ bucket.
93
+ # It is also possible to assign a flash message as part of the redirection.
94
+ # There are two special accessors for the commonly used flash names `alert` and
95
+ # `notice` as well as a general purpose `flash` bucket.
57
96
  #
58
- # redirect_to post_url(@post), alert: "Watch it, mister!"
59
- # redirect_to post_url(@post), status: :found, notice: "Pay attention to the road"
60
- # redirect_to post_url(@post), status: 301, flash: { updated_post_id: @post.id }
61
- # redirect_to({ action: 'atom' }, alert: "Something serious happened")
97
+ # redirect_to post_url(@post), alert: "Watch it, mister!"
98
+ # redirect_to post_url(@post), status: :found, notice: "Pay attention to the road"
99
+ # redirect_to post_url(@post), status: 301, flash: { updated_post_id: @post.id }
100
+ # redirect_to({ action: 'atom' }, alert: "Something serious happened")
62
101
  #
63
- # Statements after +redirect_to+ in our controller get executed, so +redirect_to+ doesn't stop the execution of the function.
64
- # To terminate the execution of the function immediately after the +redirect_to+, use return.
102
+ # Statements after `redirect_to` in our controller get executed, so
103
+ # `redirect_to` doesn't stop the execution of the function. To terminate the
104
+ # execution of the function immediately after the `redirect_to`, use return.
65
105
  #
66
- # redirect_to post_url(@post) and return
106
+ # redirect_to post_url(@post) and return
67
107
  #
68
- # === Open Redirect protection
108
+ # ### Open Redirect protection
69
109
  #
70
- # By default, \Rails protects against redirecting to external hosts for your app's safety, so called open redirects.
71
- # 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>
110
+ # By default, Rails protects against redirecting to external hosts for your
111
+ # app's safety, so called open redirects.
72
112
  #
73
113
  # Here #redirect_to automatically validates the potentially-unsafe URL:
74
114
  #
75
- # redirect_to params[:redirect_url]
115
+ # redirect_to params[:redirect_url]
116
+ #
117
+ # The `action_on_open_redirect` configuration option controls the behavior when an unsafe
118
+ # redirect is detected:
119
+ # * `:log` - Logs a warning but allows the redirect
120
+ # * `:notify` - Sends an Active Support notification for monitoring
121
+ # * `:raise` - Raises an UnsafeRedirectError
122
+ #
123
+ # To allow any external redirects pass `allow_other_host: true`, though using a
124
+ # user-provided param in that case is unsafe.
125
+ #
126
+ # redirect_to "https://rubyonrails.org", allow_other_host: true
76
127
  #
77
- # Raises UnsafeRedirectError in the case of an unsafe redirect.
128
+ # See #url_from for more information on what an internal and safe URL is, or how
129
+ # to fall back to an alternate redirect URL in the unsafe case.
78
130
  #
79
- # To allow any external redirects pass <tt>allow_other_host: true</tt>, though using a user-provided param in that case is unsafe.
131
+ # ### Path Relative URL Redirect Protection
80
132
  #
81
- # redirect_to "https://rubyonrails.org", allow_other_host: true
133
+ # Rails also protects against potentially unsafe path relative URL redirects that don't
134
+ # start with a leading slash. These can create security vulnerabilities:
82
135
  #
83
- # 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.
136
+ # redirect_to "example.com" # Creates http://yourdomain.comexample.com
137
+ # redirect_to "@attacker.com" # Creates http://yourdomain.com@attacker.com
138
+ # # which browsers interpret as user@host
139
+ #
140
+ # You can configure how Rails handles these cases using:
141
+ #
142
+ # config.action_controller.action_on_path_relative_redirect = :log # default
143
+ # config.action_controller.action_on_path_relative_redirect = :notify
144
+ # config.action_controller.action_on_path_relative_redirect = :raise
145
+ #
146
+ # * `:log` - Logs a warning but allows the redirect
147
+ # * `:notify` - Sends an Active Support notification but allows the redirect
148
+ # (includes stack trace to help identify the source)
149
+ # * `:raise` - Raises an UnsafeRedirectError
84
150
  def redirect_to(options = {}, response_options = {})
85
151
  raise ActionControllerError.new("Cannot redirect to nil!") unless options
86
152
  raise AbstractController::DoubleRenderError if response_body
87
153
 
88
- allow_other_host = response_options.delete(:allow_other_host) { _allow_other_host }
154
+ allow_other_host = response_options.delete(:allow_other_host)
89
155
 
90
- self.status = _extract_redirect_to_status(options, response_options)
156
+ proposed_status = _extract_redirect_to_status(options, response_options)
91
157
 
92
158
  redirect_to_location = _compute_redirect_to_location(request, options)
93
159
  _ensure_url_is_http_header_safe(redirect_to_location)
94
160
 
95
161
  self.location = _enforce_open_redirect_protection(redirect_to_location, allow_other_host: allow_other_host)
96
162
  self.response_body = ""
163
+ self.status = proposed_status
97
164
  end
98
165
 
99
- # Soft deprecated alias for #redirect_back_or_to where the +fallback_location+ location is supplied as a keyword argument instead
100
- # of the first positional argument.
166
+ # Soft deprecated alias for #redirect_back_or_to where the `fallback_location`
167
+ # location is supplied as a keyword argument instead of the first positional
168
+ # argument.
101
169
  def redirect_back(fallback_location:, allow_other_host: _allow_other_host, **args)
102
170
  redirect_back_or_to fallback_location, allow_other_host: allow_other_host, **args
103
171
  end
104
172
 
105
- # Redirects the browser to the page that issued the request (the referrer)
106
- # if possible, otherwise redirects to the provided default fallback
107
- # location.
173
+ # Redirects the browser to the page that issued the request (the referrer) if
174
+ # possible, otherwise redirects to the provided default fallback location.
175
+ #
176
+ # The referrer information is pulled from the HTTP `Referer` (sic) header on the
177
+ # request. This is an optional header and its presence on the request is subject
178
+ # to browser security settings and user preferences. If the request is missing
179
+ # this header, the `fallback_location` will be used.
108
180
  #
109
- # The referrer information is pulled from the HTTP +Referer+ (sic) header on
110
- # the request. This is an optional header and its presence on the request is
111
- # subject to browser security settings and user preferences. If the request
112
- # is missing this header, the <tt>fallback_location</tt> will be used.
181
+ # redirect_back_or_to({ action: "show", id: 5 })
182
+ # redirect_back_or_to @post
183
+ # redirect_back_or_to "http://www.rubyonrails.org"
184
+ # redirect_back_or_to "/images/screenshot.jpg"
185
+ # redirect_back_or_to posts_url
186
+ # redirect_back_or_to proc { edit_post_url(@post) }
187
+ # redirect_back_or_to '/', allow_other_host: false
113
188
  #
114
- # redirect_back_or_to({ action: "show", id: 5 })
115
- # redirect_back_or_to @post
116
- # redirect_back_or_to "http://www.rubyonrails.org"
117
- # redirect_back_or_to "/images/screenshot.jpg"
118
- # redirect_back_or_to posts_url
119
- # redirect_back_or_to proc { edit_post_url(@post) }
120
- # redirect_back_or_to '/', allow_other_host: false
189
+ # #### Options
190
+ # * `:allow_other_host` - Allow or disallow redirection to the host that is
191
+ # different to the current host, defaults to true.
121
192
  #
122
- # ==== Options
123
- # * <tt>:allow_other_host</tt> - Allow or disallow redirection to the host that is different to the current host, defaults to true.
124
193
  #
125
- # All other options that can be passed to #redirect_to are accepted as
126
- # options, and the behavior is identical.
194
+ # All other options that can be passed to #redirect_to are accepted as options,
195
+ # and the behavior is identical.
127
196
  def redirect_back_or_to(fallback_location, allow_other_host: _allow_other_host, **options)
128
197
  if request.referer && (allow_other_host || _url_host_allowed?(request.referer))
129
198
  redirect_to request.referer, allow_other_host: allow_other_host, **options
130
199
  else
131
- # The method level `allow_other_host` doesn't apply in the fallback case, omit and let the `redirect_to` handling take over.
200
+ # The method level `allow_other_host` doesn't apply in the fallback case, omit
201
+ # and let the `redirect_to` handling take over.
132
202
  redirect_to fallback_location, **options
133
203
  end
134
204
  end
135
205
 
136
206
  def _compute_redirect_to_location(request, options) # :nodoc:
137
207
  case options
138
- # The scheme name consist of a letter followed by any combination of
139
- # letters, digits, and the plus ("+"), period ("."), or hyphen ("-")
140
- # characters; and is terminated by a colon (":").
141
- # See https://tools.ietf.org/html/rfc3986#section-3.1
142
- # The protocol relative scheme starts with a double slash "//".
208
+ # The scheme name consist of a letter followed by any combination of letters,
209
+ # digits, and the plus ("+"), period ("."), or hyphen ("-") characters; and is
210
+ # terminated by a colon (":"). See
211
+ # https://tools.ietf.org/html/rfc3986#section-3.1 The protocol relative scheme
212
+ # starts with a double slash "//".
143
213
  when /\A([a-z][a-z\d\-+.]*:|\/\/).*/i
144
214
  options.to_str
145
215
  when String
216
+ if !options.start_with?("/", "?") && !options.empty?
217
+ _handle_path_relative_redirect(options)
218
+ end
219
+
146
220
  request.protocol + request.host_with_port + options
147
221
  when Proc
148
222
  _compute_redirect_to_location request, instance_eval(&options)
@@ -153,25 +227,30 @@ module ActionController
153
227
  module_function :_compute_redirect_to_location
154
228
  public :_compute_redirect_to_location
155
229
 
156
- # Verifies the passed +location+ is an internal URL that's safe to redirect to and returns it, or nil if not.
157
- # Useful to wrap a params provided redirect URL and fall back to an alternate URL to redirect to:
230
+ # Verifies the passed `location` is an internal URL that's safe to redirect to
231
+ # and returns it, or nil if not. Useful to wrap a params provided redirect URL
232
+ # and fall back to an alternate URL to redirect to:
158
233
  #
159
- # redirect_to url_from(params[:redirect_url]) || root_url
234
+ # redirect_to url_from(params[:redirect_url]) || root_url
160
235
  #
161
- # The +location+ is considered internal, and safe, if it's on the same host as <tt>request.host</tt>:
236
+ # The `location` is considered internal, and safe, if it's on the same host as
237
+ # `request.host`:
162
238
  #
163
- # # If request.host is example.com:
164
- # url_from("https://example.com/profile") # => "https://example.com/profile"
165
- # url_from("http://example.com/profile") # => "http://example.com/profile"
166
- # url_from("http://evil.com/profile") # => nil
239
+ # # If request.host is example.com:
240
+ # url_from("https://example.com/profile") # => "https://example.com/profile"
241
+ # url_from("http://example.com/profile") # => "http://example.com/profile"
242
+ # url_from("http://evil.com/profile") # => nil
167
243
  #
168
244
  # Subdomains are considered part of the host:
169
245
  #
170
- # # If request.host is on https://example.com or https://app.example.com, you'd get:
171
- # url_from("https://dev.example.com/profile") # => nil
246
+ # # If request.host is on https://example.com or https://app.example.com, you'd get:
247
+ # url_from("https://dev.example.com/profile") # => nil
172
248
  #
173
- # 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>.
174
- # However, #url_from is meant to take an external parameter to verify as in <tt>url_from(params[:redirect_url])</tt>.
249
+ # NOTE: there's a similarity with
250
+ # [url_for](rdoc-ref:ActionDispatch::Routing::UrlFor#url_for), which generates
251
+ # an internal URL from various options from within the app, e.g.
252
+ # `url_for(@post)`. However, #url_from is meant to take an external parameter to
253
+ # verify as in `url_from(params[:redirect_url])`.
175
254
  def url_from(location)
176
255
  location = location.presence
177
256
  location if location && _url_host_allowed?(location)
@@ -179,47 +258,87 @@ module ActionController
179
258
 
180
259
  private
181
260
  def _allow_other_host
182
- !raise_on_open_redirects
261
+ return false if raise_on_open_redirects
262
+
263
+ action_on_open_redirect != :raise
183
264
  end
184
265
 
185
266
  def _extract_redirect_to_status(options, response_options)
186
267
  if options.is_a?(Hash) && options.key?(:status)
187
- Rack::Utils.status_code(options.delete(:status))
268
+ ActionDispatch::Response.rack_status_code(options.delete(:status))
188
269
  elsif response_options.key?(:status)
189
- Rack::Utils.status_code(response_options[:status])
270
+ ActionDispatch::Response.rack_status_code(response_options[:status])
190
271
  else
191
272
  302
192
273
  end
193
274
  end
194
275
 
195
276
  def _enforce_open_redirect_protection(location, allow_other_host:)
277
+ # Explictly allowed other host or host is in allow list allow redirect
196
278
  if allow_other_host || _url_host_allowed?(location)
197
279
  location
280
+ # Explicitly disallowed other host
281
+ elsif allow_other_host == false
282
+ raise OpenRedirectError.new(location)
283
+ # Configuration disallows other hosts
284
+ elsif !_allow_other_host
285
+ raise OpenRedirectError.new(location)
286
+ # Log but allow redirect
287
+ elsif action_on_open_redirect == :log
288
+ logger.warn "Open redirect to #{location.inspect} detected" if logger
289
+ location
290
+ # Notify but allow redirect
291
+ elsif action_on_open_redirect == :notify
292
+ ActiveSupport::Notifications.instrument("open_redirect.action_controller",
293
+ location: location,
294
+ request: request,
295
+ stack_trace: caller,
296
+ )
297
+ location
298
+ # Fall through, should not happen but raise for safety
198
299
  else
199
- raise UnsafeRedirectError, "Unsafe redirect to #{location.truncate(100).inspect}, pass allow_other_host: true to redirect anyway."
300
+ raise OpenRedirectError.new(location)
200
301
  end
201
302
  end
202
303
 
203
304
  def _url_host_allowed?(url)
204
- host = URI(url.to_s).host
305
+ url_to_s = url.to_s
306
+ host = URI(url_to_s).host
205
307
 
206
- return true if host == request.host
207
- return false unless host.nil?
208
- return false unless url.to_s.start_with?("/")
209
- !url.to_s.start_with?("//")
308
+ if host.nil?
309
+ url_to_s.start_with?("/") && !url_to_s.start_with?("//")
310
+ else
311
+ host == request.host || self.class.allowed_redirect_hosts_permissions&.allows?(host)
312
+ end
210
313
  rescue ArgumentError, URI::Error
211
314
  false
212
315
  end
213
316
 
214
317
  def _ensure_url_is_http_header_safe(url)
215
- # Attempt to comply with the set of valid token characters
216
- # defined for an HTTP header value in
217
- # https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6
318
+ # Attempt to comply with the set of valid token characters defined for an HTTP
319
+ # header value in https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6
218
320
  if url.match?(ILLEGAL_HEADER_VALUE_REGEX)
219
321
  msg = "The redirect URL #{url} contains one or more illegal HTTP header field character. " \
220
322
  "Set of legal characters defined in https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6"
221
323
  raise UnsafeRedirectError, msg
222
324
  end
223
325
  end
326
+
327
+ def _handle_path_relative_redirect(url)
328
+ message = "Path relative URL redirect detected: #{url.inspect}"
329
+
330
+ case action_on_path_relative_redirect
331
+ when :log
332
+ logger&.warn message
333
+ when :notify
334
+ ActiveSupport::Notifications.instrument("unsafe_redirect.action_controller",
335
+ url: url,
336
+ message: message,
337
+ stack_trace: caller
338
+ )
339
+ when :raise
340
+ raise PathRelativeRedirectError.new(url)
341
+ end
342
+ end
224
343
  end
225
344
  end