actionpack 5.0.0.beta1.1 → 5.0.0.beta2

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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +86 -28
  3. data/MIT-LICENSE +1 -1
  4. data/lib/abstract_controller/base.rb +2 -2
  5. data/lib/abstract_controller/rendering.rb +5 -5
  6. data/lib/action_controller.rb +4 -0
  7. data/lib/action_controller/api.rb +1 -1
  8. data/lib/action_controller/api/api_rendering.rb +14 -0
  9. data/lib/action_controller/metal.rb +2 -1
  10. data/lib/action_controller/metal/conditional_get.rb +1 -1
  11. data/lib/action_controller/metal/head.rb +0 -1
  12. data/lib/action_controller/metal/mime_responds.rb +9 -4
  13. data/lib/action_controller/metal/renderers.rb +75 -32
  14. data/lib/action_controller/metal/request_forgery_protection.rb +54 -11
  15. data/lib/action_controller/metal/strong_parameters.rb +33 -10
  16. data/lib/action_controller/test_case.rb +8 -8
  17. data/lib/action_dispatch.rb +2 -1
  18. data/lib/action_dispatch/http/cache.rb +10 -2
  19. data/lib/action_dispatch/http/headers.rb +15 -1
  20. data/lib/action_dispatch/http/mime_negotiation.rb +3 -3
  21. data/lib/action_dispatch/http/mime_type.rb +38 -47
  22. data/lib/action_dispatch/http/parameters.rb +1 -1
  23. data/lib/action_dispatch/http/request.rb +1 -1
  24. data/lib/action_dispatch/http/response.rb +8 -1
  25. data/lib/action_dispatch/journey/path/pattern.rb +1 -1
  26. data/lib/action_dispatch/middleware/ssl.rb +23 -17
  27. data/lib/action_dispatch/middleware/stack.rb +9 -0
  28. data/lib/action_dispatch/middleware/static.rb +5 -1
  29. data/lib/action_dispatch/request/session.rb +3 -3
  30. data/lib/action_dispatch/routing.rb +2 -1
  31. data/lib/action_dispatch/routing/inspector.rb +22 -10
  32. data/lib/action_dispatch/routing/mapper.rb +41 -35
  33. data/lib/action_dispatch/routing/route_set.rb +11 -2
  34. data/lib/action_dispatch/testing/assertion_response.rb +49 -0
  35. data/lib/action_dispatch/testing/assertions/response.rb +14 -14
  36. data/lib/action_dispatch/testing/test_process.rb +0 -1
  37. data/lib/action_pack.rb +1 -1
  38. data/lib/action_pack/gem_version.rb +1 -1
  39. metadata +12 -9
@@ -81,6 +81,10 @@ module ActionController #:nodoc:
81
81
  config_accessor :forgery_protection_origin_check
82
82
  self.forgery_protection_origin_check = false
83
83
 
84
+ # Controls whether form-action/method specific CSRF tokens are used.
85
+ config_accessor :per_form_csrf_tokens
86
+ self.per_form_csrf_tokens = false
87
+
84
88
  helper_method :form_authenticity_token
85
89
  helper_method :protect_against_forgery?
86
90
  end
@@ -277,16 +281,25 @@ module ActionController #:nodoc:
277
281
  end
278
282
 
279
283
  # Sets the token value for the current session.
280
- def form_authenticity_token
281
- masked_authenticity_token(session)
284
+ def form_authenticity_token(form_options: {})
285
+ masked_authenticity_token(session, form_options: form_options)
282
286
  end
283
287
 
284
288
  # Creates a masked version of the authenticity token that varies
285
289
  # on each request. The masking is used to mitigate SSL attacks
286
290
  # like BREACH.
287
- def masked_authenticity_token(session)
291
+ def masked_authenticity_token(session, form_options: {})
292
+ action, method = form_options.values_at(:action, :method)
293
+
294
+ raw_token = if per_form_csrf_tokens && action && method
295
+ action_path = normalize_action_path(action)
296
+ per_form_csrf_token(session, action_path, method)
297
+ else
298
+ real_csrf_token(session)
299
+ end
300
+
288
301
  one_time_pad = SecureRandom.random_bytes(AUTHENTICITY_TOKEN_LENGTH)
289
- encrypted_csrf_token = xor_byte_strings(one_time_pad, real_csrf_token(session))
302
+ encrypted_csrf_token = xor_byte_strings(one_time_pad, raw_token)
290
303
  masked_token = one_time_pad + encrypted_csrf_token
291
304
  Base64.strict_encode64(masked_token)
292
305
  end
@@ -316,28 +329,54 @@ module ActionController #:nodoc:
316
329
  compare_with_real_token masked_token, session
317
330
 
318
331
  elsif masked_token.length == AUTHENTICITY_TOKEN_LENGTH * 2
319
- # Split the token into the one-time pad and the encrypted
320
- # value and decrypt it
321
- one_time_pad = masked_token[0...AUTHENTICITY_TOKEN_LENGTH]
322
- encrypted_csrf_token = masked_token[AUTHENTICITY_TOKEN_LENGTH..-1]
323
- csrf_token = xor_byte_strings(one_time_pad, encrypted_csrf_token)
324
-
325
- compare_with_real_token csrf_token, session
332
+ csrf_token = unmask_token(masked_token)
326
333
 
334
+ compare_with_real_token(csrf_token, session) ||
335
+ valid_per_form_csrf_token?(csrf_token, session)
327
336
  else
328
337
  false # Token is malformed
329
338
  end
330
339
  end
331
340
 
341
+ def unmask_token(masked_token)
342
+ # Split the token into the one-time pad and the encrypted
343
+ # value and decrypt it
344
+ one_time_pad = masked_token[0...AUTHENTICITY_TOKEN_LENGTH]
345
+ encrypted_csrf_token = masked_token[AUTHENTICITY_TOKEN_LENGTH..-1]
346
+ xor_byte_strings(one_time_pad, encrypted_csrf_token)
347
+ end
348
+
332
349
  def compare_with_real_token(token, session)
333
350
  ActiveSupport::SecurityUtils.secure_compare(token, real_csrf_token(session))
334
351
  end
335
352
 
353
+ def valid_per_form_csrf_token?(token, session)
354
+ if per_form_csrf_tokens
355
+ correct_token = per_form_csrf_token(
356
+ session,
357
+ normalize_action_path(request.fullpath),
358
+ request.request_method
359
+ )
360
+
361
+ ActiveSupport::SecurityUtils.secure_compare(token, correct_token)
362
+ else
363
+ false
364
+ end
365
+ end
366
+
336
367
  def real_csrf_token(session)
337
368
  session[:_csrf_token] ||= SecureRandom.base64(AUTHENTICITY_TOKEN_LENGTH)
338
369
  Base64.strict_decode64(session[:_csrf_token])
339
370
  end
340
371
 
372
+ def per_form_csrf_token(session, action_path, method)
373
+ OpenSSL::HMAC.digest(
374
+ OpenSSL::Digest::SHA256.new,
375
+ real_csrf_token(session),
376
+ [action_path, method.downcase].join("#")
377
+ )
378
+ end
379
+
341
380
  def xor_byte_strings(s1, s2)
342
381
  s1.bytes.zip(s2.bytes).map { |(c1,c2)| c1 ^ c2 }.pack('c*')
343
382
  end
@@ -362,5 +401,9 @@ module ActionController #:nodoc:
362
401
  true
363
402
  end
364
403
  end
404
+
405
+ def normalize_action_path(action_path)
406
+ action_path.split('?').first.to_s.chomp('/')
407
+ end
365
408
  end
366
409
  end
@@ -109,7 +109,8 @@ module ActionController
109
109
  cattr_accessor :permit_all_parameters, instance_accessor: false
110
110
  cattr_accessor :action_on_unpermitted_parameters, instance_accessor: false
111
111
 
112
- delegate :keys, :key?, :has_key?, :empty?, :inspect, to: :@parameters
112
+ delegate :keys, :key?, :has_key?, :values, :has_value?, :value?, :empty?, :include?, :inspect,
113
+ :as_json, to: :@parameters
113
114
 
114
115
  # By default, never raise an UnpermittedParameters exception if these
115
116
  # params are present. The default includes both 'controller' and 'action'
@@ -159,7 +160,11 @@ module ActionController
159
160
  if other_hash.respond_to?(:permitted?)
160
161
  super
161
162
  else
162
- @parameters == other_hash
163
+ if other_hash.is_a?(Hash)
164
+ @parameters == other_hash.with_indifferent_access
165
+ else
166
+ @parameters == other_hash
167
+ end
163
168
  end
164
169
  end
165
170
 
@@ -176,7 +181,7 @@ module ActionController
176
181
  # safe_params.to_h # => {"name"=>"Senjougahara Hitagi"}
177
182
  def to_h
178
183
  if permitted?
179
- convert_parameters_to_hashes(@parameters)
184
+ convert_parameters_to_hashes(@parameters, :to_h)
180
185
  else
181
186
  slice(*self.class.always_permitted_parameters).permit!.to_h
182
187
  end
@@ -186,7 +191,7 @@ module ActionController
186
191
  # <tt>ActiveSupport::HashWithIndifferentAccess</tt> representation of this
187
192
  # parameter.
188
193
  def to_unsafe_h
189
- convert_parameters_to_hashes(@parameters)
194
+ convert_parameters_to_hashes(@parameters, :to_unsafe_h)
190
195
  end
191
196
  alias_method :to_unsafe_hash, :to_unsafe_h
192
197
 
@@ -419,7 +424,7 @@ module ActionController
419
424
  # params.fetch(:none) # => ActionController::ParameterMissing: param is missing or the value is empty: none
420
425
  # params.fetch(:none, 'Francesco') # => "Francesco"
421
426
  # params.fetch(:none) { 'Francesco' } # => "Francesco"
422
- def fetch(key, *args, &block)
427
+ def fetch(key, *args)
423
428
  convert_value_to_parameters(
424
429
  @parameters.fetch(key) {
425
430
  if block_given?
@@ -514,7 +519,7 @@ module ActionController
514
519
  # to key. If the key is not found, returns the default value. If the
515
520
  # optional code block is given and the key is not found, pass in the key
516
521
  # and return the result of block.
517
- def delete(key, &block)
522
+ def delete(key)
518
523
  convert_value_to_parameters(@parameters.delete(key))
519
524
  end
520
525
 
@@ -579,6 +584,24 @@ module ActionController
579
584
  dup
580
585
  end
581
586
 
587
+ def method_missing(method_sym, *args, &block)
588
+ if @parameters.respond_to?(method_sym)
589
+ message = <<-DEPRECATE.squish
590
+ Method #{method_sym} is deprecated and will be removed in Rails 5.1,
591
+ as `ActionController::Parameters` no longer inherits from
592
+ hash. Using this deprecated behavior exposes potential security
593
+ problems. If you continue to use this method you may be creating
594
+ a security vulnerability in your app that can be exploited. Instead,
595
+ consider using one of these documented methods which are not
596
+ deprecated: http://api.rubyonrails.org/v#{ActionPack.version}/classes/ActionController/Parameters.html
597
+ DEPRECATE
598
+ ActiveSupport::Deprecation.warn(message)
599
+ @parameters.public_send(method_sym, *args, &block)
600
+ else
601
+ super
602
+ end
603
+ end
604
+
582
605
  protected
583
606
  def permitted=(new_permitted)
584
607
  @permitted = new_permitted
@@ -595,16 +618,16 @@ module ActionController
595
618
  end
596
619
  end
597
620
 
598
- def convert_parameters_to_hashes(value)
621
+ def convert_parameters_to_hashes(value, using)
599
622
  case value
600
623
  when Array
601
- value.map { |v| convert_parameters_to_hashes(v) }
624
+ value.map { |v| convert_parameters_to_hashes(v, using) }
602
625
  when Hash
603
626
  value.transform_values do |v|
604
- convert_parameters_to_hashes(v)
627
+ convert_parameters_to_hashes(v, using)
605
628
  end.with_indifferent_access
606
629
  when Parameters
607
- value.to_h
630
+ value.send(using)
608
631
  else
609
632
  value
610
633
  end
@@ -8,6 +8,10 @@ require 'rails-dom-testing'
8
8
 
9
9
  module ActionController
10
10
  # :stopdoc:
11
+ class Metal
12
+ include Testing::Functional
13
+ end
14
+
11
15
  # ActionController::TestCase will be deprecated and moved to a gem in Rails 5.1.
12
16
  # Please use ActionDispatch::IntegrationTest going forward.
13
17
  class TestRequest < ActionDispatch::TestRequest #:nodoc:
@@ -455,16 +459,16 @@ module ActionController
455
459
  parameters = nil
456
460
  end
457
461
 
458
- if parameters.present? || session.present? || flash.present?
462
+ if parameters || session || flash
459
463
  non_kwarg_request_warning
460
464
  end
461
465
  end
462
466
 
463
- if body.present?
467
+ if body
464
468
  @request.set_header 'RAW_POST_DATA', body
465
469
  end
466
470
 
467
- if http_method.present?
471
+ if http_method
468
472
  http_method = http_method.to_s.upcase
469
473
  else
470
474
  http_method = "GET"
@@ -472,16 +476,12 @@ module ActionController
472
476
 
473
477
  parameters ||= {}
474
478
 
475
- if format.present?
479
+ if format
476
480
  parameters[:format] = format
477
481
  end
478
482
 
479
483
  @html_document = nil
480
484
 
481
- unless @controller.respond_to?(:recycle!)
482
- @controller.extend(Testing::Functional)
483
- end
484
-
485
485
  self.cookies.update @request.cookies
486
486
  self.cookies.update_cookies_from_jar
487
487
  @request.set_header 'HTTP_COOKIE', cookies.to_header
@@ -1,5 +1,5 @@
1
1
  #--
2
- # Copyright (c) 2004-2015 David Heinemeier Hansson
2
+ # Copyright (c) 2004-2016 David Heinemeier Hansson
3
3
  #
4
4
  # Permission is hereby granted, free of charge, to any person obtaining
5
5
  # a copy of this software and associated documentation files (the
@@ -95,6 +95,7 @@ module ActionDispatch
95
95
  autoload :TestProcess
96
96
  autoload :TestRequest
97
97
  autoload :TestResponse
98
+ autoload :AssertionResponse
98
99
  end
99
100
  end
100
101
 
@@ -80,9 +80,17 @@ module ActionDispatch
80
80
  set_header DATE, utc_time.httpdate
81
81
  end
82
82
 
83
+ # This method allows you to set the ETag for cached content, which
84
+ # will be returned to the end user.
85
+ #
86
+ # By default, Action Dispatch sets all ETags to be weak.
87
+ # This ensures that if the content changes only semantically,
88
+ # the whole page doesn't have to be regenerated from scratch
89
+ # by the web server. With strong ETags, pages are compared
90
+ # byte by byte, and are regenerated only if they are not exactly equal.
83
91
  def etag=(etag)
84
92
  key = ActiveSupport::Cache.expand_cache_key(etag)
85
- super %("#{Digest::MD5.hexdigest(key)}")
93
+ super %(W/"#{Digest::MD5.hexdigest(key)}")
86
94
  end
87
95
 
88
96
  def etag?; etag; end
@@ -91,7 +99,7 @@ module ActionDispatch
91
99
 
92
100
  DATE = 'Date'.freeze
93
101
  LAST_MODIFIED = "Last-Modified".freeze
94
- SPECIAL_KEYS = Set.new(%w[extras no-cache max-age public must-revalidate])
102
+ SPECIAL_KEYS = Set.new(%w[extras no-cache max-age public private must-revalidate])
95
103
 
96
104
  def cache_control_segments
97
105
  if cache_control = _cache_control
@@ -2,9 +2,23 @@ module ActionDispatch
2
2
  module Http
3
3
  # Provides access to the request's HTTP headers from the environment.
4
4
  #
5
- # env = { "CONTENT_TYPE" => "text/plain" }
5
+ # env = { "CONTENT_TYPE" => "text/plain", "HTTP_USER_AGENT" => "curl/7.43.0" }
6
6
  # headers = ActionDispatch::Http::Headers.new(env)
7
7
  # headers["Content-Type"] # => "text/plain"
8
+ # headers["User-Agent"] # => "curl/7/43/0"
9
+ #
10
+ # Also note that when headers are mapped to CGI-like variables by the Rack
11
+ # server, both dashes and underscores are converted to underscores. This
12
+ # ambiguity cannot be resolved at this stage anymore. Both underscores and
13
+ # dashes have to be interpreted as if they were originally sent as dashes.
14
+ #
15
+ # # GET / HTTP/1.1
16
+ # # ...
17
+ # # User-Agent: curl/7.43.0
18
+ # # X_Custom_Header: token
19
+ #
20
+ # headers["X_Custom_Header"] # => nil
21
+ # headers["X-Custom-Header"] # => "token"
8
22
  class Headers
9
23
  CGI_VARIABLES = Set.new(%W[
10
24
  AUTH_TYPE
@@ -67,10 +67,10 @@ module ActionDispatch
67
67
 
68
68
  v = if params_readable
69
69
  Array(Mime[parameters[:format]])
70
- elsif format = format_from_path_extension
71
- Array(Mime[format])
72
70
  elsif use_accept_header && valid_accept_header
73
71
  accepts
72
+ elsif extension_format = format_from_path_extension
73
+ [extension_format]
74
74
  elsif xhr?
75
75
  [Mime[:js]]
76
76
  else
@@ -166,7 +166,7 @@ module ActionDispatch
166
166
  def format_from_path_extension
167
167
  path = @env['action_dispatch.original_path'] || @env['PATH_INFO']
168
168
  if match = path && path.match(/\.(\w+)\z/)
169
- match.captures.first
169
+ Mime[match.captures.first]
170
170
  end
171
171
  end
172
172
  end
@@ -1,3 +1,5 @@
1
+ # -*- frozen-string-literal: true -*-
2
+
1
3
  require 'singleton'
2
4
  require 'active_support/core_ext/module/attribute_accessors'
3
5
  require 'active_support/core_ext/string/starts_ends_with'
@@ -106,70 +108,58 @@ module Mime
106
108
  result = @index <=> item.index if result == 0
107
109
  result
108
110
  end
109
-
110
- def ==(item)
111
- @name == item.to_s
112
- end
113
111
  end
114
112
 
115
- class AcceptList < Array #:nodoc:
116
- def assort!
117
- sort!
113
+ class AcceptList #:nodoc:
114
+ def self.sort!(list)
115
+ list.sort!
116
+
117
+ text_xml_idx = find_item_by_name list, 'text/xml'
118
+ app_xml_idx = find_item_by_name list, Mime[:xml].to_s
118
119
 
119
120
  # Take care of the broken text/xml entry by renaming or deleting it
120
121
  if text_xml_idx && app_xml_idx
122
+ app_xml = list[app_xml_idx]
123
+ text_xml = list[text_xml_idx]
124
+
121
125
  app_xml.q = [text_xml.q, app_xml.q].max # set the q value to the max of the two
122
- exchange_xml_items if app_xml_idx > text_xml_idx # make sure app_xml is ahead of text_xml in the list
123
- delete_at(text_xml_idx) # delete text_xml from the list
126
+ if app_xml_idx > text_xml_idx # make sure app_xml is ahead of text_xml in the list
127
+ list[app_xml_idx], list[text_xml_idx] = text_xml, app_xml
128
+ app_xml_idx, text_xml_idx = text_xml_idx, app_xml_idx
129
+ end
130
+ list.delete_at(text_xml_idx) # delete text_xml from the list
124
131
  elsif text_xml_idx
125
- text_xml.name = Mime[:xml].to_s
132
+ list[text_xml_idx].name = Mime[:xml].to_s
126
133
  end
127
134
 
128
135
  # Look for more specific XML-based types and sort them ahead of app/xml
129
136
  if app_xml_idx
137
+ app_xml = list[app_xml_idx]
130
138
  idx = app_xml_idx
131
139
 
132
- while idx < length
133
- type = self[idx]
140
+ while idx < list.length
141
+ type = list[idx]
134
142
  break if type.q < app_xml.q
135
143
 
136
144
  if type.name.ends_with? '+xml'
137
- self[app_xml_idx], self[idx] = self[idx], app_xml
138
- @app_xml_idx = idx
145
+ list[app_xml_idx], list[idx] = list[idx], app_xml
146
+ app_xml_idx = idx
139
147
  end
140
148
  idx += 1
141
149
  end
142
150
  end
143
151
 
144
- map! { |i| Mime::Type.lookup(i.name) }.uniq!
145
- to_a
152
+ list.map! { |i| Mime::Type.lookup(i.name) }.uniq!
153
+ list
146
154
  end
147
155
 
148
- private
149
- def text_xml_idx
150
- @text_xml_idx ||= index('text/xml')
151
- end
152
-
153
- def app_xml_idx
154
- @app_xml_idx ||= index(Mime[:xml].to_s)
155
- end
156
-
157
- def text_xml
158
- self[text_xml_idx]
159
- end
160
-
161
- def app_xml
162
- self[app_xml_idx]
163
- end
164
-
165
- def exchange_xml_items
166
- self[app_xml_idx], self[text_xml_idx] = text_xml, app_xml
167
- @app_xml_idx, @text_xml_idx = text_xml_idx, app_xml_idx
168
- end
156
+ def self.find_item_by_name(array, name)
157
+ array.index { |item| item.name == name }
158
+ end
169
159
  end
170
160
 
171
161
  class << self
172
- TRAILING_STAR_REGEXP = /(text|application)\/\*/
162
+ TRAILING_STAR_REGEXP = /^(text|application)\/\*/
173
163
  PARAMETER_SEPARATOR_REGEXP = /;\s*\w+="?\w+"?/
174
164
 
175
165
  def register_callback(&block)
@@ -209,21 +199,22 @@ module Mime
209
199
  accept_header = accept_header.split(PARAMETER_SEPARATOR_REGEXP).first
210
200
  parse_trailing_star(accept_header) || [Mime::Type.lookup(accept_header)].compact
211
201
  else
212
- list, index = AcceptList.new, 0
202
+ list, index = [], 0
213
203
  accept_header.split(',').each do |header|
214
204
  params, q = header.split(PARAMETER_SEPARATOR_REGEXP)
215
- if params.present?
216
- params.strip!
217
205
 
218
- params = parse_trailing_star(params) || [params]
206
+ next unless params
207
+ params.strip!
208
+ next if params.empty?
209
+
210
+ params = parse_trailing_star(params) || [params]
219
211
 
220
- params.each do |m|
221
- list << AcceptItem.new(index, m.to_s, q)
222
- index += 1
223
- end
212
+ params.each do |m|
213
+ list << AcceptItem.new(index, m.to_s, q)
214
+ index += 1
224
215
  end
225
216
  end
226
- list.assort!
217
+ AcceptList.sort! list
227
218
  end
228
219
  end
229
220