actionpack 5.2.1 → 7.0.2.4

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of actionpack might be problematic. Click here for more details.

Files changed (167) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +264 -220
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +6 -6
  5. data/lib/abstract_controller/asset_paths.rb +1 -1
  6. data/lib/abstract_controller/base.rb +24 -4
  7. data/lib/abstract_controller/caching/fragments.rb +8 -24
  8. data/lib/abstract_controller/caching.rb +2 -2
  9. data/lib/abstract_controller/callbacks.rb +34 -8
  10. data/lib/abstract_controller/collector.rb +5 -4
  11. data/lib/abstract_controller/error.rb +1 -1
  12. data/lib/abstract_controller/helpers.rb +107 -90
  13. data/lib/abstract_controller/logger.rb +1 -1
  14. data/lib/abstract_controller/railties/routes_helpers.rb +19 -1
  15. data/lib/abstract_controller/rendering.rb +9 -9
  16. data/lib/abstract_controller/translation.rb +12 -5
  17. data/lib/abstract_controller/url_for.rb +4 -6
  18. data/lib/abstract_controller.rb +2 -0
  19. data/lib/action_controller/api.rb +5 -4
  20. data/lib/action_controller/base.rb +6 -9
  21. data/lib/action_controller/caching.rb +1 -3
  22. data/lib/action_controller/log_subscriber.rb +13 -9
  23. data/lib/action_controller/metal/basic_implicit_render.rb +1 -1
  24. data/lib/action_controller/metal/conditional_get.rb +57 -6
  25. data/lib/action_controller/metal/content_security_policy.rb +2 -3
  26. data/lib/action_controller/metal/cookies.rb +4 -2
  27. data/lib/action_controller/metal/data_streaming.rb +9 -18
  28. data/lib/action_controller/metal/default_headers.rb +17 -0
  29. data/lib/action_controller/metal/etag_with_template_digest.rb +4 -6
  30. data/lib/action_controller/metal/exceptions.rb +55 -12
  31. data/lib/action_controller/metal/flash.rb +10 -6
  32. data/lib/action_controller/metal/head.rb +7 -4
  33. data/lib/action_controller/metal/helpers.rb +15 -6
  34. data/lib/action_controller/metal/http_authentication.rb +41 -39
  35. data/lib/action_controller/metal/implicit_render.rb +5 -15
  36. data/lib/action_controller/metal/instrumentation.rb +59 -55
  37. data/lib/action_controller/metal/live.rb +80 -33
  38. data/lib/action_controller/metal/logging.rb +20 -0
  39. data/lib/action_controller/metal/mime_responds.rb +22 -7
  40. data/lib/action_controller/metal/parameter_encoding.rb +35 -4
  41. data/lib/action_controller/metal/params_wrapper.rb +50 -31
  42. data/lib/action_controller/metal/permissions_policy.rb +46 -0
  43. data/lib/action_controller/metal/redirecting.rb +93 -23
  44. data/lib/action_controller/metal/renderers.rb +4 -4
  45. data/lib/action_controller/metal/rendering.rb +14 -9
  46. data/lib/action_controller/metal/request_forgery_protection.rb +160 -58
  47. data/lib/action_controller/metal/rescue.rb +2 -2
  48. data/lib/action_controller/metal/streaming.rb +1 -4
  49. data/lib/action_controller/metal/strong_parameters.rb +236 -88
  50. data/lib/action_controller/metal/testing.rb +9 -2
  51. data/lib/action_controller/metal/url_for.rb +1 -1
  52. data/lib/action_controller/metal.rb +16 -17
  53. data/lib/action_controller/railtie.rb +49 -6
  54. data/lib/action_controller/railties/helpers.rb +1 -1
  55. data/lib/action_controller/renderer.rb +37 -13
  56. data/lib/action_controller/template_assertions.rb +1 -1
  57. data/lib/action_controller/test_case.rb +98 -68
  58. data/lib/action_controller.rb +4 -5
  59. data/lib/action_dispatch/http/cache.rb +45 -32
  60. data/lib/action_dispatch/http/content_disposition.rb +45 -0
  61. data/lib/action_dispatch/http/content_security_policy.rb +69 -56
  62. data/lib/action_dispatch/http/filter_parameters.rb +14 -8
  63. data/lib/action_dispatch/http/filter_redirect.rb +2 -3
  64. data/lib/action_dispatch/http/headers.rb +4 -4
  65. data/lib/action_dispatch/http/mime_negotiation.rb +44 -16
  66. data/lib/action_dispatch/http/mime_type.rb +47 -30
  67. data/lib/action_dispatch/http/parameters.rb +18 -27
  68. data/lib/action_dispatch/http/permissions_policy.rb +173 -0
  69. data/lib/action_dispatch/http/request.rb +49 -35
  70. data/lib/action_dispatch/http/response.rb +34 -26
  71. data/lib/action_dispatch/http/upload.rb +9 -1
  72. data/lib/action_dispatch/http/url.rb +86 -94
  73. data/lib/action_dispatch/journey/formatter.rb +55 -31
  74. data/lib/action_dispatch/journey/gtg/builder.rb +30 -46
  75. data/lib/action_dispatch/journey/gtg/simulator.rb +15 -8
  76. data/lib/action_dispatch/journey/gtg/transition_table.rb +78 -21
  77. data/lib/action_dispatch/journey/nfa/dot.rb +0 -11
  78. data/lib/action_dispatch/journey/nodes/node.rb +83 -16
  79. data/lib/action_dispatch/journey/parser.rb +13 -13
  80. data/lib/action_dispatch/journey/parser.y +1 -1
  81. data/lib/action_dispatch/journey/path/pattern.rb +42 -34
  82. data/lib/action_dispatch/journey/route.rb +14 -31
  83. data/lib/action_dispatch/journey/router/utils.rb +16 -14
  84. data/lib/action_dispatch/journey/router.rb +27 -35
  85. data/lib/action_dispatch/journey/routes.rb +3 -5
  86. data/lib/action_dispatch/journey/scanner.rb +10 -4
  87. data/lib/action_dispatch/journey/visitors.rb +1 -4
  88. data/lib/action_dispatch/journey/visualizer/fsm.js +49 -24
  89. data/lib/action_dispatch/journey/visualizer/index.html.erb +1 -1
  90. data/lib/action_dispatch/journey.rb +0 -2
  91. data/lib/action_dispatch/middleware/actionable_exceptions.rb +45 -0
  92. data/lib/action_dispatch/middleware/callbacks.rb +2 -4
  93. data/lib/action_dispatch/middleware/cookies.rb +136 -113
  94. data/lib/action_dispatch/middleware/debug_exceptions.rb +47 -68
  95. data/lib/action_dispatch/middleware/debug_locks.rb +8 -8
  96. data/lib/action_dispatch/middleware/debug_view.rb +66 -0
  97. data/lib/action_dispatch/middleware/exception_wrapper.rb +79 -30
  98. data/lib/action_dispatch/middleware/executor.rb +4 -1
  99. data/lib/action_dispatch/middleware/flash.rb +10 -12
  100. data/lib/action_dispatch/middleware/host_authorization.rb +159 -0
  101. data/lib/action_dispatch/middleware/public_exceptions.rb +6 -3
  102. data/lib/action_dispatch/middleware/remote_ip.rb +30 -20
  103. data/lib/action_dispatch/middleware/request_id.rb +5 -6
  104. data/lib/action_dispatch/middleware/server_timing.rb +33 -0
  105. data/lib/action_dispatch/middleware/session/abstract_store.rb +16 -3
  106. data/lib/action_dispatch/middleware/session/cache_store.rb +11 -6
  107. data/lib/action_dispatch/middleware/session/cookie_store.rb +24 -19
  108. data/lib/action_dispatch/middleware/show_exceptions.rb +20 -11
  109. data/lib/action_dispatch/middleware/ssl.rb +20 -15
  110. data/lib/action_dispatch/middleware/stack.rb +79 -7
  111. data/lib/action_dispatch/middleware/static.rb +150 -94
  112. data/lib/action_dispatch/middleware/templates/rescues/_actions.html.erb +13 -0
  113. data/lib/action_dispatch/middleware/templates/rescues/_actions.text.erb +0 -0
  114. data/lib/action_dispatch/middleware/templates/rescues/_message_and_suggestions.html.erb +22 -0
  115. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +6 -11
  116. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb +1 -1
  117. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +4 -2
  118. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +46 -36
  119. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +8 -0
  120. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb +7 -0
  121. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +25 -6
  122. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +1 -1
  123. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +9 -6
  124. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +4 -1
  125. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +121 -15
  126. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +19 -0
  127. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb +3 -0
  128. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +5 -5
  129. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +4 -4
  130. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +5 -5
  131. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +4 -4
  132. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +16 -2
  133. data/lib/action_dispatch/railtie.rb +16 -4
  134. data/lib/action_dispatch/request/session.rb +59 -22
  135. data/lib/action_dispatch/request/utils.rb +28 -2
  136. data/lib/action_dispatch/routing/inspector.rb +102 -54
  137. data/lib/action_dispatch/routing/mapper.rb +184 -156
  138. data/lib/action_dispatch/routing/polymorphic_routes.rb +21 -19
  139. data/lib/action_dispatch/routing/redirection.rb +4 -6
  140. data/lib/action_dispatch/routing/route_set.rb +83 -73
  141. data/lib/action_dispatch/routing/routes_proxy.rb +1 -1
  142. data/lib/action_dispatch/routing/url_for.rb +2 -3
  143. data/lib/action_dispatch/routing.rb +23 -22
  144. data/lib/action_dispatch/system_test_case.rb +65 -16
  145. data/lib/action_dispatch/system_testing/browser.rb +43 -16
  146. data/lib/action_dispatch/system_testing/driver.rb +42 -10
  147. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +58 -12
  148. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +3 -10
  149. data/lib/action_dispatch/testing/assertion_response.rb +0 -1
  150. data/lib/action_dispatch/testing/assertions/response.rb +4 -7
  151. data/lib/action_dispatch/testing/assertions/routing.rb +20 -8
  152. data/lib/action_dispatch/testing/assertions.rb +3 -6
  153. data/lib/action_dispatch/testing/integration.rb +61 -30
  154. data/lib/action_dispatch/testing/request_encoder.rb +2 -2
  155. data/lib/action_dispatch/testing/test_process.rb +8 -6
  156. data/lib/action_dispatch/testing/test_request.rb +3 -3
  157. data/lib/action_dispatch/testing/test_response.rb +4 -32
  158. data/lib/action_dispatch.rb +15 -7
  159. data/lib/action_pack/gem_version.rb +4 -4
  160. data/lib/action_pack.rb +1 -1
  161. metadata +44 -25
  162. data/lib/action_controller/metal/force_ssl.rb +0 -99
  163. data/lib/action_dispatch/http/parameter_filter.rb +0 -86
  164. data/lib/action_dispatch/journey/nfa/builder.rb +0 -78
  165. data/lib/action_dispatch/journey/nfa/simulator.rb +0 -49
  166. data/lib/action_dispatch/journey/nfa/transition_table.rb +0 -120
  167. data/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb +0 -26
@@ -7,6 +7,12 @@ module ActionController
7
7
  include AbstractController::Logger
8
8
  include ActionController::UrlFor
9
9
 
10
+ class UnsafeRedirectError < StandardError; end
11
+
12
+ included do
13
+ mattr_accessor :raise_on_open_redirects, default: false
14
+ end
15
+
10
16
  # Redirects the browser to the target specified in +options+. This parameter can be any one of:
11
17
  #
12
18
  # * <tt>Hash</tt> - The URL will be generated by calling url_for with the +options+.
@@ -54,16 +60,42 @@ module ActionController
54
60
  #
55
61
  # Statements after +redirect_to+ in our controller get executed, so +redirect_to+ doesn't stop the execution of the function.
56
62
  # To terminate the execution of the function immediately after the +redirect_to+, use return.
63
+ #
57
64
  # redirect_to post_url(@post) and return
58
- def redirect_to(options = {}, response_status = {})
65
+ #
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.
82
+ def redirect_to(options = {}, response_options = {})
59
83
  raise ActionControllerError.new("Cannot redirect to nil!") unless options
60
84
  raise AbstractController::DoubleRenderError if response_body
61
85
 
62
- self.status = _extract_redirect_to_status(options, response_status)
63
- self.location = _compute_redirect_to_location(request, options)
86
+ allow_other_host = response_options.delete(:allow_other_host) { _allow_other_host }
87
+
88
+ self.status = _extract_redirect_to_status(options, response_options)
89
+ self.location = _enforce_open_redirect_protection(_compute_redirect_to_location(request, options), allow_other_host: allow_other_host)
64
90
  self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(response.location)}\">redirected</a>.</body></html>"
65
91
  end
66
92
 
93
+ # Soft deprecated alias for #redirect_back_or_to where the +fallback_location+ location is supplied as a keyword argument instead
94
+ # of the first positional argument.
95
+ def redirect_back(fallback_location:, allow_other_host: _allow_other_host, **args)
96
+ redirect_back_or_to fallback_location, allow_other_host: allow_other_host, **args
97
+ end
98
+
67
99
  # Redirects the browser to the page that issued the request (the referrer)
68
100
  # if possible, otherwise redirects to the provided default fallback
69
101
  # location.
@@ -73,39 +105,41 @@ module ActionController
73
105
  # subject to browser security settings and user preferences. If the request
74
106
  # is missing this header, the <tt>fallback_location</tt> will be used.
75
107
  #
76
- # redirect_back fallback_location: { action: "show", id: 5 }
77
- # redirect_back fallback_location: @post
78
- # redirect_back fallback_location: "http://www.rubyonrails.org"
79
- # redirect_back fallback_location: "/images/screenshot.jpg"
80
- # redirect_back fallback_location: posts_url
81
- # redirect_back fallback_location: proc { edit_post_url(@post) }
82
- # redirect_back fallback_location: '/', allow_other_host: false
108
+ # redirect_back_or_to({ action: "show", id: 5 })
109
+ # redirect_back_or_to @post
110
+ # redirect_back_or_to "http://www.rubyonrails.org"
111
+ # redirect_back_or_to "/images/screenshot.jpg"
112
+ # redirect_back_or_to posts_url
113
+ # redirect_back_or_to proc { edit_post_url(@post) }
114
+ # redirect_back_or_to '/', allow_other_host: false
83
115
  #
84
116
  # ==== Options
85
- # * <tt>:fallback_location</tt> - The default fallback location that will be used on missing +Referer+ header.
86
117
  # * <tt>:allow_other_host</tt> - Allow or disallow redirection to the host that is different to the current host, defaults to true.
87
118
  #
88
- # All other options that can be passed to <tt>redirect_to</tt> are accepted as
119
+ # All other options that can be passed to #redirect_to are accepted as
89
120
  # options and the behavior is identical.
90
- def redirect_back(fallback_location:, allow_other_host: true, **args)
91
- referer = request.headers["Referer"]
92
- redirect_to_referer = referer && (allow_other_host || _url_host_allowed?(referer))
93
- redirect_to redirect_to_referer ? referer : fallback_location, **args
121
+ def redirect_back_or_to(fallback_location, allow_other_host: _allow_other_host, **options)
122
+ if request.referer && (allow_other_host || _url_host_allowed?(request.referer))
123
+ redirect_to request.referer, allow_other_host: allow_other_host, **options
124
+ else
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
127
+ end
94
128
  end
95
129
 
96
- def _compute_redirect_to_location(request, options) #:nodoc:
130
+ def _compute_redirect_to_location(request, options) # :nodoc:
97
131
  case options
98
132
  # The scheme name consist of a letter followed by any combination of
99
133
  # letters, digits, and the plus ("+"), period ("."), or hyphen ("-")
100
134
  # characters; and is terminated by a colon (":").
101
135
  # See https://tools.ietf.org/html/rfc3986#section-3.1
102
136
  # The protocol relative scheme starts with a double slash "//".
103
- when /\A([a-z][a-z\d\-+\.]*:|\/\/).*/i
104
- options
137
+ when /\A([a-z][a-z\d\-+.]*:|\/\/).*/i
138
+ options.to_str
105
139
  when String
106
140
  request.protocol + request.host_with_port + options
107
141
  when Proc
108
- _compute_redirect_to_location request, options.call
142
+ _compute_redirect_to_location request, instance_eval(&options)
109
143
  else
110
144
  url_for(options)
111
145
  end.delete("\0\r\n")
@@ -113,17 +147,53 @@ module ActionController
113
147
  module_function :_compute_redirect_to_location
114
148
  public :_compute_redirect_to_location
115
149
 
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
+
116
174
  private
117
- def _extract_redirect_to_status(options, response_status)
175
+ def _allow_other_host
176
+ !raise_on_open_redirects
177
+ end
178
+
179
+ def _extract_redirect_to_status(options, response_options)
118
180
  if options.is_a?(Hash) && options.key?(:status)
119
181
  Rack::Utils.status_code(options.delete(:status))
120
- elsif response_status.key?(:status)
121
- Rack::Utils.status_code(response_status[:status])
182
+ elsif response_options.key?(:status)
183
+ Rack::Utils.status_code(response_options[:status])
122
184
  else
123
185
  302
124
186
  end
125
187
  end
126
188
 
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
+
127
197
  def _url_host_allowed?(url)
128
198
  URI(url.to_s).host == request.host
129
199
  rescue ArgumentError, URI::Error
@@ -157,24 +157,24 @@ module ActionController
157
157
  json = json.to_json(options) unless json.kind_of?(String)
158
158
 
159
159
  if options[:callback].present?
160
- if content_type.nil? || content_type == Mime[:json]
160
+ if media_type.nil? || media_type == Mime[:json]
161
161
  self.content_type = Mime[:js]
162
162
  end
163
163
 
164
164
  "/**/#{options[:callback]}(#{json})"
165
165
  else
166
- self.content_type ||= Mime[:json]
166
+ self.content_type = Mime[:json] if media_type.nil?
167
167
  json
168
168
  end
169
169
  end
170
170
 
171
171
  add :js do |js, options|
172
- self.content_type ||= Mime[:js]
172
+ self.content_type = Mime[:js] if media_type.nil?
173
173
  js.respond_to?(:to_js) ? js.to_js(options) : js
174
174
  end
175
175
 
176
176
  add :xml do |xml, options|
177
- self.content_type ||= Mime[:xml]
177
+ self.content_type = Mime[:xml] if media_type.nil?
178
178
  xml.respond_to?(:to_xml) ? xml.to_xml(options) : xml
179
179
  end
180
180
  end
@@ -24,14 +24,8 @@ module ActionController
24
24
  end
25
25
  end
26
26
 
27
- # Before processing, set the request formats in current controller formats.
28
- def process_action(*) #:nodoc:
29
- self.formats = request.formats.map(&:ref).compact
30
- super
31
- end
32
-
33
27
  # Check for double render errors and set the content_type after rendering.
34
- def render(*args) #:nodoc:
28
+ def render(*args) # :nodoc:
35
29
  raise ::AbstractController::DoubleRenderError if response_body
36
30
  super
37
31
  end
@@ -40,7 +34,7 @@ module ActionController
40
34
  def render_to_string(*)
41
35
  result = super
42
36
  if result.respond_to?(:each)
43
- string = "".dup
37
+ string = +""
44
38
  result.each { |r| string << r }
45
39
  string
46
40
  else
@@ -53,6 +47,11 @@ module ActionController
53
47
  end
54
48
 
55
49
  private
50
+ # Before processing, set the request formats in current controller formats.
51
+ def process_action(*) # :nodoc:
52
+ self.formats = request.formats.filter_map(&:ref)
53
+ super
54
+ end
56
55
 
57
56
  def _process_variant(options)
58
57
  if defined?(request) && !request.nil? && request.variant.present?
@@ -73,11 +72,17 @@ module ActionController
73
72
  end
74
73
 
75
74
  def _set_rendered_content_type(format)
76
- if format && !response.content_type
75
+ if format && !response.media_type
77
76
  self.content_type = format.to_s
78
77
  end
79
78
  end
80
79
 
80
+ def _set_vary_header
81
+ if self.headers["Vary"].blank? && request.should_apply_vary_header?
82
+ self.headers["Vary"] = "Accept"
83
+ end
84
+ end
85
+
81
86
  # Normalize arguments by catching blocks and setting them on :update.
82
87
  def _normalize_args(action = nil, options = {}, &blk)
83
88
  options = super
@@ -3,13 +3,12 @@
3
3
  require "rack/session/abstract/id"
4
4
  require "action_controller/metal/exceptions"
5
5
  require "active_support/security_utils"
6
- require "active_support/core_ext/string/strip"
7
6
 
8
- module ActionController #:nodoc:
9
- class InvalidAuthenticityToken < ActionControllerError #:nodoc:
7
+ module ActionController # :nodoc:
8
+ class InvalidAuthenticityToken < ActionControllerError # :nodoc:
10
9
  end
11
10
 
12
- class InvalidCrossOriginRequest < ActionControllerError #:nodoc:
11
+ class InvalidCrossOriginRequest < ActionControllerError # :nodoc:
13
12
  end
14
13
 
15
14
  # Controller actions are protected from Cross-Site Request Forgery (CSRF) attacks
@@ -18,7 +17,7 @@ module ActionController #:nodoc:
18
17
  # access. When a request reaches your application, \Rails verifies the received
19
18
  # token with the token in the session. All requests are checked except GET requests
20
19
  # as these should be idempotent. Keep in mind that all session-oriented requests
21
- # should be CSRF protected, including JavaScript and HTML requests.
20
+ # are CSRF protected by default, including JavaScript and HTML requests.
22
21
  #
23
22
  # Since HTML and JavaScript requests are typically made from the browser, we
24
23
  # need to ensure to verify request authenticity for the web browser. We can
@@ -31,31 +30,30 @@ module ActionController #:nodoc:
31
30
  # URL on your site. When your JavaScript response loads on their site, it executes.
32
31
  # With carefully crafted JavaScript on their end, sensitive data in your JavaScript
33
32
  # response may be extracted. To prevent this, only XmlHttpRequest (known as XHR or
34
- # Ajax) requests are allowed to make GET requests for JavaScript responses.
33
+ # Ajax) requests are allowed to make requests for JavaScript responses.
35
34
  #
36
- # It's important to remember that XML or JSON requests are also affected and if
37
- # you're building an API you should change forgery protection method in
38
- # <tt>ApplicationController</tt> (by default: <tt>:exception</tt>):
35
+ # Subclasses of <tt>ActionController::Base</tt> are protected by default with the
36
+ # <tt>:exception</tt> strategy, which raises an
37
+ # <tt>ActionController::InvalidAuthenticityToken</tt> error on unverified requests.
38
+ #
39
+ # APIs may want to disable this behavior since they are typically designed to be
40
+ # state-less: that is, the request API client handles the session instead of Rails.
41
+ # One way to achieve this is to use the <tt>:null_session</tt> strategy instead,
42
+ # which allows unverified requests to be handled, but with an empty session:
39
43
  #
40
44
  # class ApplicationController < ActionController::Base
41
- # protect_from_forgery unless: -> { request.format.json? }
45
+ # protect_from_forgery with: :null_session
42
46
  # end
43
47
  #
44
- # CSRF protection is turned on with the <tt>protect_from_forgery</tt> method.
45
- # By default <tt>protect_from_forgery</tt> protects your session with
46
- # <tt>:null_session</tt> method, which provides an empty session
47
- # during request.
48
- #
49
- # We may want to disable CSRF protection for APIs since they are typically
50
- # designed to be state-less. That is, the request API client will handle
51
- # the session for you instead of Rails.
48
+ # Note that API only applications don't include this module or a session middleware
49
+ # by default, and so don't require CSRF protection to be configured.
52
50
  #
53
51
  # The token parameter is named <tt>authenticity_token</tt> by default. The name and
54
52
  # value of this token must be added to every layout that renders forms by including
55
53
  # <tt>csrf_meta_tags</tt> in the HTML +head+.
56
54
  #
57
55
  # Learn more about CSRF attacks and securing your application in the
58
- # {Ruby on Rails Security Guide}[http://guides.rubyonrails.org/security.html].
56
+ # {Ruby on Rails Security Guide}[https://guides.rubyonrails.org/security.html].
59
57
  module RequestForgeryProtection
60
58
  extend ActiveSupport::Concern
61
59
 
@@ -92,6 +90,19 @@ module ActionController #:nodoc:
92
90
  config_accessor :default_protect_from_forgery
93
91
  self.default_protect_from_forgery = false
94
92
 
93
+ # Controls whether URL-safe CSRF tokens are generated.
94
+ config_accessor :urlsafe_csrf_tokens, instance_writer: false
95
+ self.urlsafe_csrf_tokens = true
96
+
97
+ singleton_class.redefine_method(:urlsafe_csrf_tokens=) do |urlsafe_csrf_tokens|
98
+ if urlsafe_csrf_tokens
99
+ ActiveSupport::Deprecation.warn("URL-safe CSRF tokens are now the default. Use 6.1 defaults or above.")
100
+ else
101
+ ActiveSupport::Deprecation.warn("Non-URL-safe CSRF tokens are deprecated. Use 6.1 defaults or above.")
102
+ end
103
+ config.urlsafe_csrf_tokens = urlsafe_csrf_tokens
104
+ end
105
+
95
106
  helper_method :form_authenticity_token
96
107
  helper_method :protect_against_forgery?
97
108
  end
@@ -122,10 +133,26 @@ module ActionController #:nodoc:
122
133
  # If you need to add verification to the beginning of the callback chain, use <tt>prepend: true</tt>.
123
134
  # * <tt>:with</tt> - Set the method to handle unverified request.
124
135
  #
125
- # Valid unverified request handling methods are:
136
+ # Built-in unverified request handling methods are:
126
137
  # * <tt>:exception</tt> - Raises ActionController::InvalidAuthenticityToken exception.
127
138
  # * <tt>:reset_session</tt> - Resets the session.
128
139
  # * <tt>:null_session</tt> - Provides an empty session during request but doesn't reset it completely. Used as default if <tt>:with</tt> option is not specified.
140
+ #
141
+ # You can also implement custom strategy classes for unverified request handling:
142
+ #
143
+ # class CustomStrategy
144
+ # def initialize(controller)
145
+ # @controller = controller
146
+ # end
147
+ #
148
+ # def handle_unverified_request
149
+ # # Custom behaviour for unverfied request
150
+ # end
151
+ # end
152
+ #
153
+ # class ApplicationController < ActionController:x:Base
154
+ # protect_from_forgery with: CustomStrategy
155
+ # end
129
156
  def protect_from_forgery(options = {})
130
157
  options = options.reverse_merge(prepend: false)
131
158
 
@@ -145,11 +172,19 @@ module ActionController #:nodoc:
145
172
  end
146
173
 
147
174
  private
148
-
149
175
  def protection_method_class(name)
150
- ActionController::RequestForgeryProtection::ProtectionMethods.const_get(name.to_s.classify)
151
- rescue NameError
152
- raise ArgumentError, "Invalid request forgery protection method, use :null_session, :exception, or :reset_session"
176
+ case name
177
+ when :null_session
178
+ ProtectionMethods::NullSession
179
+ when :reset_session
180
+ ProtectionMethods::ResetSession
181
+ when :exception
182
+ ProtectionMethods::Exception
183
+ when Class
184
+ name
185
+ else
186
+ raise ArgumentError, "Invalid request forgery protection method, use :null_session, :exception, :reset_session, or a custom forgery protection class."
187
+ end
153
188
  end
154
189
  end
155
190
 
@@ -169,8 +204,7 @@ module ActionController #:nodoc:
169
204
  end
170
205
 
171
206
  private
172
-
173
- class NullSessionHash < Rack::Session::Abstract::SessionHash #:nodoc:
207
+ class NullSessionHash < Rack::Session::Abstract::SessionHash # :nodoc:
174
208
  def initialize(req)
175
209
  super(nil, req)
176
210
  @data = {}
@@ -183,9 +217,13 @@ module ActionController #:nodoc:
183
217
  def exists?
184
218
  true
185
219
  end
220
+
221
+ def enabled?
222
+ false
223
+ end
186
224
  end
187
225
 
188
- class NullCookieJar < ActionDispatch::Cookies::CookieJar #:nodoc:
226
+ class NullCookieJar < ActionDispatch::Cookies::CookieJar # :nodoc:
189
227
  def write(*)
190
228
  # nothing
191
229
  end
@@ -203,12 +241,14 @@ module ActionController #:nodoc:
203
241
  end
204
242
 
205
243
  class Exception
244
+ attr_accessor :warning_message
245
+
206
246
  def initialize(controller)
207
247
  @controller = controller
208
248
  end
209
249
 
210
250
  def handle_unverified_request
211
- raise ActionController::InvalidAuthenticityToken
251
+ raise ActionController::InvalidAuthenticityToken, warning_message
212
252
  end
213
253
  end
214
254
  end
@@ -228,22 +268,31 @@ module ActionController #:nodoc:
228
268
  mark_for_same_origin_verification!
229
269
 
230
270
  if !verified_request?
231
- if logger && log_warning_on_csrf_failure
232
- if valid_request_origin?
233
- logger.warn "Can't verify CSRF token authenticity."
234
- else
235
- logger.warn "HTTP Origin header (#{request.origin}) didn't match request.base_url (#{request.base_url})"
236
- end
237
- end
271
+ logger.warn unverified_request_warning_message if logger && log_warning_on_csrf_failure
272
+
238
273
  handle_unverified_request
239
274
  end
240
275
  end
241
276
 
242
277
  def handle_unverified_request # :doc:
243
- forgery_protection_strategy.new(self).handle_unverified_request
278
+ protection_strategy = forgery_protection_strategy.new(self)
279
+
280
+ if protection_strategy.respond_to?(:warning_message)
281
+ protection_strategy.warning_message = unverified_request_warning_message
282
+ end
283
+
284
+ protection_strategy.handle_unverified_request
244
285
  end
245
286
 
246
- #:nodoc:
287
+ def unverified_request_warning_message # :nodoc:
288
+ if valid_request_origin?
289
+ "Can't verify CSRF token authenticity."
290
+ else
291
+ "HTTP Origin header (#{request.origin}) didn't match request.base_url (#{request.base_url})"
292
+ end
293
+ end
294
+
295
+ # :nodoc:
247
296
  CROSS_ORIGIN_JAVASCRIPT_WARNING = "Security warning: an embedded " \
248
297
  "<script> tag on another site requested protected JavaScript. " \
249
298
  "If you know what you're doing, go ahead and disable forgery " \
@@ -276,7 +325,7 @@ module ActionController #:nodoc:
276
325
 
277
326
  # Check for cross-origin JavaScript responses.
278
327
  def non_xhr_javascript_response? # :doc:
279
- content_type =~ %r(\Atext/javascript) && !request.xhr?
328
+ %r(\A(?:text|application)/javascript).match?(media_type) && !request.xhr?
280
329
  end
281
330
 
282
331
  AUTHENTICITY_TOKEN_LENGTH = 32
@@ -303,28 +352,25 @@ module ActionController #:nodoc:
303
352
  [form_authenticity_param, request.x_csrf_token]
304
353
  end
305
354
 
306
- # Sets the token value for the current session.
307
- def form_authenticity_token(form_options: {})
355
+ # Creates the authenticity token for the current request.
356
+ def form_authenticity_token(form_options: {}) # :doc:
308
357
  masked_authenticity_token(session, form_options: form_options)
309
358
  end
310
359
 
311
360
  # Creates a masked version of the authenticity token that varies
312
361
  # on each request. The masking is used to mitigate SSL attacks
313
362
  # like BREACH.
314
- def masked_authenticity_token(session, form_options: {}) # :doc:
363
+ def masked_authenticity_token(session, form_options: {})
315
364
  action, method = form_options.values_at(:action, :method)
316
365
 
317
366
  raw_token = if per_form_csrf_tokens && action && method
318
367
  action_path = normalize_action_path(action)
319
368
  per_form_csrf_token(session, action_path, method)
320
369
  else
321
- real_csrf_token(session)
370
+ global_csrf_token(session)
322
371
  end
323
372
 
324
- one_time_pad = SecureRandom.random_bytes(AUTHENTICITY_TOKEN_LENGTH)
325
- encrypted_csrf_token = xor_byte_strings(one_time_pad, raw_token)
326
- masked_token = one_time_pad + encrypted_csrf_token
327
- Base64.strict_encode64(masked_token)
373
+ mask_token(raw_token)
328
374
  end
329
375
 
330
376
  # Checks the client's masked token to see if it matches the
@@ -336,7 +382,7 @@ module ActionController #:nodoc:
336
382
  end
337
383
 
338
384
  begin
339
- masked_token = Base64.strict_decode64(encoded_masked_token)
385
+ masked_token = decode_csrf_token(encoded_masked_token)
340
386
  rescue ArgumentError # encoded_masked_token is invalid Base64
341
387
  return false
342
388
  end
@@ -354,7 +400,8 @@ module ActionController #:nodoc:
354
400
  elsif masked_token.length == AUTHENTICITY_TOKEN_LENGTH * 2
355
401
  csrf_token = unmask_token(masked_token)
356
402
 
357
- compare_with_real_token(csrf_token, session) ||
403
+ compare_with_global_token(csrf_token, session) ||
404
+ compare_with_real_token(csrf_token, session) ||
358
405
  valid_per_form_csrf_token?(csrf_token, session)
359
406
  else
360
407
  false # Token is malformed.
@@ -369,15 +416,26 @@ module ActionController #:nodoc:
369
416
  xor_byte_strings(one_time_pad, encrypted_csrf_token)
370
417
  end
371
418
 
419
+ def mask_token(raw_token) # :doc:
420
+ one_time_pad = SecureRandom.random_bytes(AUTHENTICITY_TOKEN_LENGTH)
421
+ encrypted_csrf_token = xor_byte_strings(one_time_pad, raw_token)
422
+ masked_token = one_time_pad + encrypted_csrf_token
423
+ encode_csrf_token(masked_token)
424
+ end
425
+
372
426
  def compare_with_real_token(token, session) # :doc:
373
427
  ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, real_csrf_token(session))
374
428
  end
375
429
 
430
+ def compare_with_global_token(token, session) # :doc:
431
+ ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, global_csrf_token(session))
432
+ end
433
+
376
434
  def valid_per_form_csrf_token?(token, session) # :doc:
377
435
  if per_form_csrf_tokens
378
436
  correct_token = per_form_csrf_token(
379
437
  session,
380
- normalize_action_path(request.fullpath),
438
+ request.path.chomp("/"),
381
439
  request.request_method
382
440
  )
383
441
 
@@ -388,22 +446,38 @@ module ActionController #:nodoc:
388
446
  end
389
447
 
390
448
  def real_csrf_token(session) # :doc:
391
- session[:_csrf_token] ||= SecureRandom.base64(AUTHENTICITY_TOKEN_LENGTH)
392
- Base64.strict_decode64(session[:_csrf_token])
449
+ session[:_csrf_token] ||= generate_csrf_token
450
+ decode_csrf_token(session[:_csrf_token])
393
451
  end
394
452
 
395
453
  def per_form_csrf_token(session, action_path, method) # :doc:
454
+ csrf_token_hmac(session, [action_path, method.downcase].join("#"))
455
+ end
456
+
457
+ GLOBAL_CSRF_TOKEN_IDENTIFIER = "!real_csrf_token"
458
+ private_constant :GLOBAL_CSRF_TOKEN_IDENTIFIER
459
+
460
+ def global_csrf_token(session) # :doc:
461
+ csrf_token_hmac(session, GLOBAL_CSRF_TOKEN_IDENTIFIER)
462
+ end
463
+
464
+ def csrf_token_hmac(session, identifier) # :doc:
396
465
  OpenSSL::HMAC.digest(
397
466
  OpenSSL::Digest::SHA256.new,
398
467
  real_csrf_token(session),
399
- [action_path, method.downcase].join("#")
468
+ identifier
400
469
  )
401
470
  end
402
471
 
403
472
  def xor_byte_strings(s1, s2) # :doc:
404
- s2_bytes = s2.bytes
405
- s1.each_byte.with_index { |c1, i| s2_bytes[i] ^= c1 }
406
- s2_bytes.pack("C*")
473
+ s2 = s2.dup
474
+ size = s1.bytesize
475
+ i = 0
476
+ while i < size
477
+ s2.setbyte(i, s1.getbyte(i) ^ s2.getbyte(i))
478
+ i += 1
479
+ end
480
+ s2
407
481
  end
408
482
 
409
483
  # The form's authenticity parameter. Override to provide your own.
@@ -413,14 +487,14 @@ module ActionController #:nodoc:
413
487
 
414
488
  # Checks if the controller allows forgery protection.
415
489
  def protect_against_forgery? # :doc:
416
- allow_forgery_protection
490
+ allow_forgery_protection && (!session.respond_to?(:enabled?) || session.enabled?)
417
491
  end
418
492
 
419
- NULL_ORIGIN_MESSAGE = <<-MSG.strip_heredoc
493
+ NULL_ORIGIN_MESSAGE = <<~MSG
420
494
  The browser returned a 'null' origin for a request with origin-based forgery protection turned on. This usually
421
495
  means you have the 'no-referrer' Referrer-Policy header enabled, or that the request came from a site that
422
496
  refused to give its origin. This makes it impossible for Rails to verify the source of the requests. Likely the
423
- best solution is to change your referrer policy to something less strict like same-origin or strict-same-origin.
497
+ best solution is to change your referrer policy to something less strict like same-origin or strict-origin.
424
498
  If you cannot change the referrer policy, you can disable origin checking with the
425
499
  Rails.application.config.action_controller.forgery_protection_origin_check setting.
426
500
  MSG
@@ -441,5 +515,33 @@ module ActionController #:nodoc:
441
515
  uri = URI.parse(action_path)
442
516
  uri.path.chomp("/")
443
517
  end
518
+
519
+ def generate_csrf_token # :nodoc:
520
+ if urlsafe_csrf_tokens
521
+ SecureRandom.urlsafe_base64(AUTHENTICITY_TOKEN_LENGTH)
522
+ else
523
+ SecureRandom.base64(AUTHENTICITY_TOKEN_LENGTH)
524
+ end
525
+ end
526
+
527
+ def encode_csrf_token(csrf_token) # :nodoc:
528
+ if urlsafe_csrf_tokens
529
+ Base64.urlsafe_encode64(csrf_token, padding: false)
530
+ else
531
+ Base64.strict_encode64(csrf_token)
532
+ end
533
+ end
534
+
535
+ def decode_csrf_token(encoded_csrf_token) # :nodoc:
536
+ if urlsafe_csrf_tokens
537
+ Base64.urlsafe_decode64(encoded_csrf_token)
538
+ else
539
+ begin
540
+ Base64.strict_decode64(encoded_csrf_token)
541
+ rescue ArgumentError
542
+ Base64.urlsafe_decode64(encoded_csrf_token)
543
+ end
544
+ end
545
+ end
444
546
  end
445
547
  end