rack 3.0.8 → 3.1.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,51 +3,46 @@
3
3
  require 'strscan'
4
4
 
5
5
  require_relative '../utils'
6
+ require_relative '../bad_request'
6
7
 
7
8
  module Rack
8
9
  module Multipart
9
- class MultipartPartLimitError < Errno::EMFILE; end
10
+ class MultipartPartLimitError < Errno::EMFILE
11
+ include BadRequest
12
+ end
10
13
 
11
- class MultipartTotalPartLimitError < StandardError; end
14
+ class MultipartTotalPartLimitError < StandardError
15
+ include BadRequest
16
+ end
12
17
 
13
18
  # Use specific error class when parsing multipart request
14
19
  # that ends early.
15
- class EmptyContentError < ::EOFError; end
20
+ class EmptyContentError < ::EOFError
21
+ include BadRequest
22
+ end
16
23
 
17
24
  # Base class for multipart exceptions that do not subclass from
18
25
  # other exception classes for backwards compatibility.
19
- class Error < StandardError; end
26
+ class BoundaryTooLongError < StandardError
27
+ include BadRequest
28
+ end
29
+
30
+ # Prefer to use the BoundaryTooLongError class or Rack::BadRequest.
31
+ Error = BoundaryTooLongError
20
32
 
21
33
  EOL = "\r\n"
22
34
  MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni
23
- TOKEN = /[^\s()<>,;:\\"\/\[\]?=]+/
24
- CONDISP = /Content-Disposition:\s*#{TOKEN}\s*/i
25
- VALUE = /"(?:\\"|[^"])*"|#{TOKEN}/
26
- BROKEN = /^#{CONDISP}.*;\s*filename=(#{VALUE})/i
27
35
  MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{EOL}/ni
28
- MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:[^:]*;\s*name=(#{VALUE})/ni
36
+ MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:(.*)(?=#{EOL}(\S|\z))/ni
29
37
  MULTIPART_CONTENT_ID = /Content-ID:\s*([^#{EOL}]*)/ni
30
- # Updated definitions from RFC 2231
31
- ATTRIBUTE_CHAR = %r{[^ \x00-\x1f\x7f)(><@,;:\\"/\[\]?='*%]}
32
- ATTRIBUTE = /#{ATTRIBUTE_CHAR}+/
33
- SECTION = /\*[0-9]+/
34
- REGULAR_PARAMETER_NAME = /#{ATTRIBUTE}#{SECTION}?/
35
- REGULAR_PARAMETER = /(#{REGULAR_PARAMETER_NAME})=(#{VALUE})/
36
- EXTENDED_OTHER_NAME = /#{ATTRIBUTE}\*[1-9][0-9]*\*/
37
- EXTENDED_OTHER_VALUE = /%[0-9a-fA-F]{2}|#{ATTRIBUTE_CHAR}/
38
- EXTENDED_OTHER_PARAMETER = /(#{EXTENDED_OTHER_NAME})=(#{EXTENDED_OTHER_VALUE}*)/
39
- EXTENDED_INITIAL_NAME = /#{ATTRIBUTE}(?:\*0)?\*/
40
- EXTENDED_INITIAL_VALUE = /[a-zA-Z0-9\-]*'[a-zA-Z0-9\-]*'#{EXTENDED_OTHER_VALUE}*/
41
- EXTENDED_INITIAL_PARAMETER = /(#{EXTENDED_INITIAL_NAME})=(#{EXTENDED_INITIAL_VALUE})/
42
- EXTENDED_PARAMETER = /#{EXTENDED_INITIAL_PARAMETER}|#{EXTENDED_OTHER_PARAMETER}/
43
- DISPPARM = /;\s*(?:#{REGULAR_PARAMETER}|#{EXTENDED_PARAMETER})\s*/
44
- RFC2183 = /^#{CONDISP}(#{DISPPARM})+$/i
45
38
 
46
39
  class Parser
47
40
  BUFSIZE = 1_048_576
48
41
  TEXT_PLAIN = "text/plain"
49
42
  TEMPFILE_FACTORY = lambda { |filename, content_type|
50
- Tempfile.new(["RackMultipart", ::File.extname(filename.gsub("\0", '%00'))])
43
+ extension = ::File.extname(filename.gsub("\0", '%00'))[0, 129]
44
+
45
+ Tempfile.new(["RackMultipart", extension])
51
46
  }
52
47
 
53
48
  class BoundedIO # :nodoc:
@@ -98,7 +93,7 @@ module Rack
98
93
  if boundary.length > 70
99
94
  # RFC 1521 Section 7.2.1 imposes a 70 character maximum for the boundary.
100
95
  # Most clients use no more than 55 characters.
101
- raise Error, "multipart boundary size too large (#{boundary.length} characters)"
96
+ raise BoundaryTooLongError, "multipart boundary size too large (#{boundary.length} characters)"
102
97
  end
103
98
 
104
99
  io = BoundedIO.new(io, content_length) if content_length
@@ -213,6 +208,8 @@ module Rack
213
208
 
214
209
  @sbuf = StringScanner.new("".dup)
215
210
  @body_regex = /(?:#{EOL}|\A)--#{Regexp.quote(boundary)}(?:#{EOL}|--)/m
211
+ @body_regex_at_end = /#{@body_regex}\z/m
212
+ @end_boundary_size = boundary.bytesize + 4 # (-- at start, -- at finish)
216
213
  @rx_max_size = boundary.bytesize + 6 # (\r\n-- at start, either \r\n or -- at finish)
217
214
  @head_regex = /(.*?#{EOL})#{EOL}/m
218
215
  end
@@ -279,7 +276,14 @@ module Rack
279
276
  @state = :MIME_HEAD
280
277
  return
281
278
  when :END_BOUNDARY
282
- # invalid multipart upload, but retry for opening boundary
279
+ # invalid multipart upload
280
+ if @sbuf.pos == @end_boundary_size && @sbuf.rest == EOL
281
+ # stop parsing a buffer if a buffer is only an end boundary.
282
+ @state = :DONE
283
+ return
284
+ end
285
+
286
+ # retry for opening boundary
283
287
  else
284
288
  # no boundary found, keep reading data
285
289
  return :want_read
@@ -297,17 +301,101 @@ module Rack
297
301
  end
298
302
  end
299
303
 
304
+ CONTENT_DISPOSITION_MAX_PARAMS = 16
305
+ CONTENT_DISPOSITION_MAX_BYTES = 1536
300
306
  def handle_mime_head
301
307
  if @sbuf.scan_until(@head_regex)
302
308
  head = @sbuf[1]
303
309
  content_type = head[MULTIPART_CONTENT_TYPE, 1]
304
- if name = head[MULTIPART_CONTENT_DISPOSITION, 1]
305
- name = dequote(name)
310
+ if (disposition = head[MULTIPART_CONTENT_DISPOSITION, 1]) &&
311
+ disposition.bytesize <= CONTENT_DISPOSITION_MAX_BYTES
312
+
313
+ # ignore actual content-disposition value (should always be form-data)
314
+ i = disposition.index(';')
315
+ disposition.slice!(0, i+1)
316
+ param = nil
317
+ num_params = 0
318
+
319
+ # Parse parameter list
320
+ while i = disposition.index('=')
321
+ # Only parse up to max parameters, to avoid potential denial of service
322
+ num_params += 1
323
+ break if num_params > CONTENT_DISPOSITION_MAX_PARAMS
324
+
325
+ # Found end of parameter name, ensure forward progress in loop
326
+ param = disposition.slice!(0, i+1)
327
+
328
+ # Remove ending equals and preceding whitespace from parameter name
329
+ param.chomp!('=')
330
+ param.lstrip!
331
+
332
+ if disposition[0] == '"'
333
+ # Parameter value is quoted, parse it, handling backslash escapes
334
+ disposition.slice!(0, 1)
335
+ value = String.new
336
+
337
+ while i = disposition.index(/(["\\])/)
338
+ c = $1
339
+
340
+ # Append all content until ending quote or escape
341
+ value << disposition.slice!(0, i)
342
+
343
+ # Remove either backslash or ending quote,
344
+ # ensures forward progress in loop
345
+ disposition.slice!(0, 1)
346
+
347
+ # stop parsing parameter value if found ending quote
348
+ break if c == '"'
349
+
350
+ escaped_char = disposition.slice!(0, 1)
351
+ if param == 'filename' && escaped_char != '"'
352
+ # Possible IE uploaded filename, append both escape backslash and value
353
+ value << c << escaped_char
354
+ else
355
+ # Other only append escaped value
356
+ value << escaped_char
357
+ end
358
+ end
359
+ else
360
+ if i = disposition.index(';')
361
+ # Parameter value unquoted (which may be invalid), value ends at semicolon
362
+ value = disposition.slice!(0, i)
363
+ else
364
+ # If no ending semicolon, assume remainder of line is value and stop
365
+ # parsing
366
+ disposition.strip!
367
+ value = disposition
368
+ disposition = ''
369
+ end
370
+ end
371
+
372
+ case param
373
+ when 'name'
374
+ name = value
375
+ when 'filename'
376
+ filename = value
377
+ when 'filename*'
378
+ filename_star = value
379
+ # else
380
+ # ignore other parameters
381
+ end
382
+
383
+ # skip trailing semicolon, to proceed to next parameter
384
+ if i = disposition.index(';')
385
+ disposition.slice!(0, i+1)
386
+ end
387
+ end
306
388
  else
307
389
  name = head[MULTIPART_CONTENT_ID, 1]
308
390
  end
309
391
 
310
- filename = get_filename(head)
392
+ if filename_star
393
+ encoding, _, filename = filename_star.split("'", 3)
394
+ filename = normalize_filename(filename || '')
395
+ filename.force_encoding(find_encoding(encoding))
396
+ elsif filename
397
+ filename = normalize_filename(filename)
398
+ end
311
399
 
312
400
  if name.nil? || name.empty?
313
401
  name = filename || "#{content_type || TEXT_PLAIN}[]".dup
@@ -322,7 +410,7 @@ module Rack
322
410
 
323
411
  def handle_mime_body
324
412
  if (body_with_boundary = @sbuf.check_until(@body_regex)) # check but do not advance the pointer yet
325
- body = body_with_boundary.sub(/#{@body_regex}\z/m, '') # remove the boundary from the string
413
+ body = body_with_boundary.sub(@body_regex_at_end, '') # remove the boundary from the string
326
414
  @collector.on_mime_body @mime_index, body
327
415
  @sbuf.pos += body.length + 2 # skip \r\n after the content
328
416
  @state = :CONSUME_TOKEN
@@ -352,39 +440,14 @@ module Rack
352
440
  end
353
441
  end
354
442
 
355
- def get_filename(head)
356
- filename = nil
357
- case head
358
- when RFC2183
359
- params = Hash[*head.scan(DISPPARM).flat_map(&:compact)]
360
-
361
- if filename = params['filename*']
362
- encoding, _, filename = filename.split("'", 3)
363
- elsif filename = params['filename']
364
- filename = $1 if filename =~ /^"(.*)"$/
365
- end
366
- when BROKEN
367
- filename = $1
368
- filename = $1 if filename =~ /^"(.*)"$/
369
- end
370
-
371
- return unless filename
372
-
443
+ def normalize_filename(filename)
373
444
  if filename.scan(/%.?.?/).all? { |s| /%[0-9a-fA-F]{2}/.match?(s) }
374
445
  filename = Utils.unescape_path(filename)
375
446
  end
376
447
 
377
448
  filename.scrub!
378
449
 
379
- if filename !~ /\\[^\\"]/
380
- filename = filename.gsub(/\\(.)/, '\1')
381
- end
382
-
383
- if encoding
384
- filename.force_encoding ::Encoding.find(encoding)
385
- end
386
-
387
- filename
450
+ filename.split(/[\/\\]/).last || String.new
388
451
  end
389
452
 
390
453
  CHARSET = "charset"
@@ -410,11 +473,7 @@ module Rack
410
473
  v.strip!
411
474
  v = v[1..-2] if v.start_with?('"') && v.end_with?('"')
412
475
  if k == "charset"
413
- encoding = begin
414
- Encoding.find v
415
- rescue ArgumentError
416
- Encoding::BINARY
417
- end
476
+ encoding = find_encoding(v)
418
477
  end
419
478
  end
420
479
  end
@@ -424,6 +483,15 @@ module Rack
424
483
  body.force_encoding(encoding)
425
484
  end
426
485
 
486
+ # Return the related Encoding object. However, because
487
+ # enc is submitted by the user, it may be invalid, so
488
+ # use a binary encoding in that case.
489
+ def find_encoding(enc)
490
+ Encoding.find enc
491
+ rescue ArgumentError
492
+ Encoding::BINARY
493
+ end
494
+
427
495
  def handle_empty_content!(content)
428
496
  if content.nil? || content.empty?
429
497
  raise EmptyContentError
@@ -6,6 +6,8 @@ require_relative 'utils'
6
6
  require_relative 'multipart/parser'
7
7
  require_relative 'multipart/generator'
8
8
 
9
+ require_relative 'bad_request'
10
+
9
11
  module Rack
10
12
  # A multipart form data parser, adapted from IOWA.
11
13
  #
@@ -13,9 +15,40 @@ module Rack
13
15
  module Multipart
14
16
  MULTIPART_BOUNDARY = "AaB03x"
15
17
 
18
+ class MissingInputError < StandardError
19
+ include BadRequest
20
+ end
21
+
22
+ # Accumulator for multipart form data, conforming to the QueryParser API.
23
+ # In future, the Parser could return the pair list directly, but that would
24
+ # change its API.
25
+ class ParamList # :nodoc:
26
+ def self.make_params
27
+ new
28
+ end
29
+
30
+ def self.normalize_params(params, key, value)
31
+ params << [key, value]
32
+ end
33
+
34
+ def initialize
35
+ @pairs = []
36
+ end
37
+
38
+ def <<(pair)
39
+ @pairs << pair
40
+ end
41
+
42
+ def to_params_hash
43
+ @pairs
44
+ end
45
+ end
46
+
16
47
  class << self
17
48
  def parse_multipart(env, params = Rack::Utils.default_query_parser)
18
- io = env[RACK_INPUT]
49
+ unless io = env[RACK_INPUT]
50
+ raise MissingInputError, "Missing input stream!"
51
+ end
19
52
 
20
53
  if content_length = env['CONTENT_LENGTH']
21
54
  content_length = content_length.to_i
@@ -1,40 +1,39 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'bad_request'
3
4
  require 'uri'
4
5
 
5
6
  module Rack
6
7
  class QueryParser
7
- DEFAULT_SEP = /[&] */n
8
- COMMON_SEP = { ";" => /[;] */n, ";," => /[;,] */n, "&" => /[&] */n }
8
+ DEFAULT_SEP = /& */n
9
+ COMMON_SEP = { ";" => /; */n, ";," => /[;,] */n, "&" => /& */n }
9
10
 
10
11
  # ParameterTypeError is the error that is raised when incoming structural
11
12
  # parameters (parsed by parse_nested_query) contain conflicting types.
12
- class ParameterTypeError < TypeError; end
13
+ class ParameterTypeError < TypeError
14
+ include BadRequest
15
+ end
13
16
 
14
17
  # InvalidParameterError is the error that is raised when incoming structural
15
18
  # parameters (parsed by parse_nested_query) contain invalid format or byte
16
19
  # sequence.
17
- class InvalidParameterError < ArgumentError; end
20
+ class InvalidParameterError < ArgumentError
21
+ include BadRequest
22
+ end
18
23
 
19
24
  # ParamsTooDeepError is the error that is raised when params are recursively
20
25
  # nested over the specified limit.
21
- class ParamsTooDeepError < RangeError; end
22
-
23
- def self.make_default(_key_space_limit=(not_deprecated = true; nil), param_depth_limit)
24
- unless not_deprecated
25
- warn("`first argument `key_space limit` is deprecated and no longer has an effect. Please call with only one argument, which will be required in a future version of Rack", uplevel: 1)
26
- end
26
+ class ParamsTooDeepError < RangeError
27
+ include BadRequest
28
+ end
27
29
 
30
+ def self.make_default(param_depth_limit)
28
31
  new Params, param_depth_limit
29
32
  end
30
33
 
31
34
  attr_reader :param_depth_limit
32
35
 
33
- def initialize(params_class, _key_space_limit=(not_deprecated = true; nil), param_depth_limit)
34
- unless not_deprecated
35
- warn("`second argument `key_space limit` is deprecated and no longer has an effect. Please call with only two arguments, which will be required in a future version of Rack", uplevel: 1)
36
- end
37
-
36
+ def initialize(params_class, param_depth_limit)
38
37
  @params_class = params_class
39
38
  @param_depth_limit = param_depth_limit
40
39
  end
@@ -194,59 +193,7 @@ module Rack
194
193
  URI.decode_www_form_component(string, encoding)
195
194
  end
196
195
 
197
- class Params
198
- def initialize
199
- @size = 0
200
- @params = {}
201
- end
202
-
203
- def [](key)
204
- @params[key]
205
- end
206
-
207
- def []=(key, value)
208
- @params[key] = value
209
- end
210
-
211
- def key?(key)
212
- @params.key?(key)
213
- end
214
-
215
- # Recursively unwraps nested `Params` objects and constructs an object
216
- # of the same shape, but using the objects' internal representations
217
- # (Ruby hashes) in place of the objects. The result is a hash consisting
218
- # purely of Ruby primitives.
219
- #
220
- # Mutation warning!
221
- #
222
- # 1. This method mutates the internal representation of the `Params`
223
- # objects in order to save object allocations.
224
- #
225
- # 2. The value you get back is a reference to the internal hash
226
- # representation, not a copy.
227
- #
228
- # 3. Because the `Params` object's internal representation is mutable
229
- # through the `#[]=` method, it is not thread safe. The result of
230
- # getting the hash representation while another thread is adding a
231
- # key to it is non-deterministic.
232
- #
233
- def to_h
234
- @params.each do |key, value|
235
- case value
236
- when self
237
- # Handle circular references gracefully.
238
- @params[key] = @params
239
- when Params
240
- @params[key] = value.to_h
241
- when Array
242
- value.map! { |v| v.kind_of?(Params) ? v.to_h : v }
243
- else
244
- # Ignore anything that is not a `Params` object or
245
- # a collection that can contain one.
246
- end
247
- end
248
- @params
249
- end
196
+ class Params < Hash
250
197
  alias_method :to_params_hash, :to_h
251
198
  end
252
199
  end
data/lib/rack/request.rb CHANGED
@@ -482,9 +482,14 @@ module Rack
482
482
 
483
483
  # Returns the data received in the query string.
484
484
  def GET
485
- if get_header(RACK_REQUEST_QUERY_STRING) == query_string
485
+ rr_query_string = get_header(RACK_REQUEST_QUERY_STRING)
486
+ query_string = self.query_string
487
+ if rr_query_string == query_string
486
488
  get_header(RACK_REQUEST_QUERY_HASH)
487
489
  else
490
+ if rr_query_string
491
+ warn "query string used for GET parsing different from current query string. Starting in Rack 3.2, Rack will used the cached GET value instead of parsing the current query string.", uplevel: 1
492
+ end
488
493
  query_hash = parse_query(query_string, '&')
489
494
  set_header(RACK_REQUEST_QUERY_STRING, query_string)
490
495
  set_header(RACK_REQUEST_QUERY_HASH, query_hash)
@@ -505,9 +510,12 @@ module Rack
505
510
 
506
511
  # If the form hash was already memoized:
507
512
  if form_hash = get_header(RACK_REQUEST_FORM_HASH)
513
+ form_input = get_header(RACK_REQUEST_FORM_INPUT)
508
514
  # And it was memoized from the same input:
509
- if get_header(RACK_REQUEST_FORM_INPUT).equal?(rack_input)
515
+ if form_input.equal?(rack_input)
510
516
  return form_hash
517
+ elsif form_input
518
+ warn "input stream used for POST parsing different from current input stream. Starting in Rack 3.2, Rack will used the cached POST value instead of parsing the current input stream.", uplevel: 1
511
519
  end
512
520
  end
513
521
 
@@ -516,7 +524,10 @@ module Rack
516
524
  set_header RACK_REQUEST_FORM_INPUT, nil
517
525
  set_header(RACK_REQUEST_FORM_HASH, {})
518
526
  elsif form_data? || parseable_data?
519
- unless set_header(RACK_REQUEST_FORM_HASH, parse_multipart)
527
+ if pairs = Rack::Multipart.parse_multipart(env, Rack::Multipart::ParamList)
528
+ set_header RACK_REQUEST_FORM_PAIRS, pairs
529
+ set_header RACK_REQUEST_FORM_HASH, expand_param_pairs(pairs)
530
+ else
520
531
  form_vars = get_header(RACK_INPUT).read
521
532
 
522
533
  # Fix for Safari Ajax postings that always append \0
@@ -605,24 +616,10 @@ module Rack
605
616
  Rack::Request.ip_filter.call(ip)
606
617
  end
607
618
 
608
- # shortcut for <tt>request.params[key]</tt>
609
- def [](key)
610
- warn("Request#[] is deprecated and will be removed in a future version of Rack. Please use request.params[] instead", uplevel: 1)
611
-
612
- params[key.to_s]
613
- end
614
-
615
- # shortcut for <tt>request.params[key] = value</tt>
616
- #
617
- # Note that modifications will not be persisted in the env. Use update_param or delete_param if you want to destructively modify params.
618
- def []=(key, value)
619
- warn("Request#[]= is deprecated and will be removed in a future version of Rack. Please use request.params[]= instead", uplevel: 1)
620
-
621
- params[key.to_s] = value
622
- end
623
-
624
619
  # like Hash#values_at
625
620
  def values_at(*keys)
621
+ warn("Request#values_at is deprecated and will be removed in a future version of Rack. Please use request.params.values_at instead", uplevel: 1)
622
+
626
623
  keys.map { |key| params[key] }
627
624
  end
628
625
 
@@ -645,14 +642,26 @@ module Rack
645
642
  end
646
643
 
647
644
  def parse_http_accept_header(header)
648
- header.to_s.split(",").each(&:strip!).map do |part|
649
- attribute, parameters = part.split(";", 2).each(&:strip!)
645
+ # It would be nice to use filter_map here, but it's Ruby 2.7+
646
+ parts = header.to_s.split(',')
647
+
648
+ parts.map! do |part|
649
+ part.strip!
650
+ next if part.empty?
651
+
652
+ attribute, parameters = part.split(';', 2)
653
+ attribute.strip!
654
+ parameters&.strip!
650
655
  quality = 1.0
651
656
  if parameters and /\Aq=([\d.]+)/ =~ parameters
652
657
  quality = $1.to_f
653
658
  end
654
659
  [attribute, quality]
655
660
  end
661
+
662
+ parts.compact!
663
+
664
+ parts
656
665
  end
657
666
 
658
667
  # Get an array of values set in the RFC 7239 `Forwarded` request header.
@@ -672,6 +681,16 @@ module Rack
672
681
  Rack::Multipart.extract_multipart(self, query_parser)
673
682
  end
674
683
 
684
+ def expand_param_pairs(pairs, query_parser = query_parser())
685
+ params = query_parser.make_params
686
+
687
+ pairs.each do |k, v|
688
+ query_parser.normalize_params(params, k, v)
689
+ end
690
+
691
+ params.to_params_hash
692
+ end
693
+
675
694
  def split_header(value)
676
695
  value ? value.strip.split(/[,\s]+/) : []
677
696
  end