/
redirecting.rb
251 lines (230 loc) · 10.6 KB
/
redirecting.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
# frozen_string_literal: true
# :markup: markdown
module ActionController
module Redirecting
extend ActiveSupport::Concern
include AbstractController::Logger
include ActionController::UrlFor
class UnsafeRedirectError < StandardError; end
ILLEGAL_HEADER_VALUE_REGEX = /[\x00-\x08\x0A-\x1F]/
included do
mattr_accessor :raise_on_open_redirects, default: false
end
# Redirects the browser to the target specified in `options`. This parameter can
# be any one of:
#
# * `Hash` - The URL will be generated by calling url_for with the `options`.
# * `Record` - The URL will be generated by calling url_for with the
# `options`, which will reference a named URL for that record.
# * `String` starting with `protocol://` (like `http://`) or a protocol
# relative reference (like `//`) - Is passed straight through as the target
# for redirection.
# * `String` not containing a protocol - The current protocol and host is
# prepended to the string.
# * `Proc` - A block that will be executed in the controller's context. Should
# return any option accepted by `redirect_to`.
#
#
# ### Examples
#
# redirect_to action: "show", id: 5
# redirect_to @post
# redirect_to "http://www.rubyonrails.org"
# redirect_to "/images/screenshot.jpg"
# redirect_to posts_url
# redirect_to proc { edit_post_url(@post) }
#
# The redirection happens as a `302 Found` header unless otherwise specified
# using the `:status` option:
#
# redirect_to post_url(@post), status: :found
# redirect_to action: 'atom', status: :moved_permanently
# redirect_to post_url(@post), status: 301
# redirect_to action: 'atom', status: 302
#
# The status code can either be a standard [HTTP Status
# code](https://www.iana.org/assignments/http-status-codes) as an integer, or a
# symbol representing the downcased, underscored and symbolized description.
# Note that the status code must be a 3xx HTTP code, or redirection will not
# occur.
#
# If you are using XHR requests other than GET or POST and redirecting after the
# request then some browsers will follow the redirect using the original request
# method. This may lead to undesirable behavior such as a double DELETE. To work
# around this you can return a `303 See Other` status code which will be
# followed using a GET request.
#
# redirect_to posts_url, status: :see_other
# redirect_to action: 'index', status: 303
#
# 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 `alert` and
# `notice` as well as a general purpose `flash` bucket.
#
# redirect_to post_url(@post), alert: "Watch it, mister!"
# redirect_to post_url(@post), status: :found, notice: "Pay attention to the road"
# redirect_to post_url(@post), status: 301, flash: { updated_post_id: @post.id }
# redirect_to({ action: 'atom' }, alert: "Something serious happened")
#
# Statements after `redirect_to` in our controller get executed, so
# `redirect_to` doesn't stop the execution of the function. To terminate the
# execution of the function immediately after the `redirect_to`, use return.
#
# redirect_to post_url(@post) and return
#
# ### Open Redirect protection
#
# By default, Rails protects against redirecting to external hosts for your
# app's safety, so called open redirects. Note: this was a new default in Rails
# 7.0, after upgrading opt-in by uncommenting the line with
# `raise_on_open_redirects` in
# `config/initializers/new_framework_defaults_7_0.rb`
#
# Here #redirect_to automatically validates the potentially-unsafe URL:
#
# redirect_to params[:redirect_url]
#
# Raises UnsafeRedirectError in the case of an unsafe redirect.
#
# To allow any external redirects pass `allow_other_host: true`, though using a
# user-provided param in that case is unsafe.
#
# redirect_to "https://rubyonrails.org", allow_other_host: true
#
# 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.
def redirect_to(options = {}, response_options = {})
raise ActionControllerError.new("Cannot redirect to nil!") unless options
raise AbstractController::DoubleRenderError if response_body
allow_other_host = response_options.delete(:allow_other_host) { _allow_other_host }
self.status = _extract_redirect_to_status(options, response_options)
redirect_to_location = _compute_redirect_to_location(request, options)
_ensure_url_is_http_header_safe(redirect_to_location)
self.location = _enforce_open_redirect_protection(redirect_to_location, allow_other_host: allow_other_host)
self.response_body = ""
end
# Soft deprecated alias for #redirect_back_or_to where the `fallback_location`
# location is supplied as a keyword argument instead of the first positional
# argument.
def redirect_back(fallback_location:, allow_other_host: _allow_other_host, **args)
redirect_back_or_to fallback_location, allow_other_host: allow_other_host, **args
end
# Redirects the browser to the page that issued the request (the referrer) if
# possible, otherwise redirects to the provided default fallback location.
#
# The referrer information is pulled from the HTTP `Referer` (sic) header on the
# request. This is an optional header and its presence on the request is subject
# to browser security settings and user preferences. If the request is missing
# this header, the `fallback_location` will be used.
#
# redirect_back_or_to({ action: "show", id: 5 })
# redirect_back_or_to @post
# redirect_back_or_to "http://www.rubyonrails.org"
# redirect_back_or_to "/images/screenshot.jpg"
# redirect_back_or_to posts_url
# redirect_back_or_to proc { edit_post_url(@post) }
# redirect_back_or_to '/', allow_other_host: false
#
# #### Options
# * `:allow_other_host` - Allow or disallow redirection to the host that is
# different to the current host, defaults to true.
#
#
# All other options that can be passed to #redirect_to are accepted as options,
# and the behavior is identical.
def redirect_back_or_to(fallback_location, allow_other_host: _allow_other_host, **options)
if request.referer && (allow_other_host || _url_host_allowed?(request.referer))
redirect_to request.referer, allow_other_host: allow_other_host, **options
else
# The method level `allow_other_host` doesn't apply in the fallback case, omit
# and let the `redirect_to` handling take over.
redirect_to fallback_location, **options
end
end
def _compute_redirect_to_location(request, options) # :nodoc:
case options
# The scheme name consist of a letter followed by any combination of letters,
# digits, and the plus ("+"), period ("."), or hyphen ("-") characters; and is
# terminated by a colon (":"). See
# https://tools.ietf.org/html/rfc3986#section-3.1 The protocol relative scheme
# starts with a double slash "//".
when /\A([a-z][a-z\d\-+.]*:|\/\/).*/i
options.to_str
when String
request.protocol + request.host_with_port + options
when Proc
_compute_redirect_to_location request, instance_eval(&options)
else
url_for(options)
end.delete("\0\r\n")
end
module_function :_compute_redirect_to_location
public :_compute_redirect_to_location
# Verifies the passed `location` is an internal URL that's safe to redirect to
# and returns it, or nil if not. Useful to wrap a params provided redirect URL
# and fall back to an alternate URL to redirect to:
#
# redirect_to url_from(params[:redirect_url]) || root_url
#
# The `location` is considered internal, and safe, if it's on the same host as
# `request.host`:
#
# # If request.host is example.com:
# url_from("https://example.com/profile") # => "https://example.com/profile"
# url_from("http://example.com/profile") # => "http://example.com/profile"
# url_from("http://evil.com/profile") # => nil
#
# Subdomains are considered part of the host:
#
# # If request.host is on https://example.com or https://app.example.com, you'd get:
# url_from("https://dev.example.com/profile") # => nil
#
# 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.
# `url_for(@post)`. However, #url_from is meant to take an external parameter to
# verify as in `url_from(params[:redirect_url])`.
def url_from(location)
location = location.presence
location if location && _url_host_allowed?(location)
end
private
def _allow_other_host
!raise_on_open_redirects
end
def _extract_redirect_to_status(options, response_options)
if options.is_a?(Hash) && options.key?(:status)
Rack::Utils.status_code(options.delete(:status))
elsif response_options.key?(:status)
Rack::Utils.status_code(response_options[:status])
else
302
end
end
def _enforce_open_redirect_protection(location, allow_other_host:)
if allow_other_host || _url_host_allowed?(location)
location
else
raise UnsafeRedirectError, "Unsafe redirect to #{location.truncate(100).inspect}, pass allow_other_host: true to redirect anyway."
end
end
def _url_host_allowed?(url)
host = URI(url.to_s).host
return true if host == request.host
return false unless host.nil?
return false unless url.to_s.start_with?("/")
!url.to_s.start_with?("//")
rescue ArgumentError, URI::Error
false
end
def _ensure_url_is_http_header_safe(url)
# Attempt to comply with the set of valid token characters defined for an HTTP
# header value in https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6
if url.match?(ILLEGAL_HEADER_VALUE_REGEX)
msg = "The redirect URL #{url} contains one or more illegal HTTP header field character. " \
"Set of legal characters defined in https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6"
raise UnsafeRedirectError, msg
end
end
end
end