rack 3.1.8 → 3.2.5

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.
@@ -14,12 +14,11 @@ module Rack
14
14
  # For more information on the use of media types in HTTP, see:
15
15
  # http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7
16
16
  def type(content_type)
17
- return nil unless content_type
18
- if type = content_type.split(SPLIT_PATTERN, 2).first
19
- type.rstrip!
20
- type.downcase!
21
- type
22
- end
17
+ return nil unless content_type && !content_type.empty?
18
+ type = content_type.split(SPLIT_PATTERN, 2).first
19
+ type.rstrip!
20
+ type.downcase!
21
+ type
23
22
  end
24
23
 
25
24
  # The media type parameters provided in CONTENT_TYPE as a Hash, or
@@ -27,8 +26,13 @@ module Rack
27
26
  # provided. e.g., when the CONTENT_TYPE is "text/plain;charset=utf-8",
28
27
  # this method responds with the following Hash:
29
28
  # { 'charset' => 'utf-8' }
29
+ #
30
+ # This will pass back parameters with empty strings in the hash if they
31
+ # lack a value (e.g., "text/plain;charset=" will return { 'charset' => '' },
32
+ # and "text/plain;charset" will return { 'charset' => '' }, similarly to
33
+ # the query params parser (barring the latter case, which returns nil instead)).
30
34
  def params(content_type)
31
- return {} if content_type.nil?
35
+ return {} if content_type.nil? || content_type.empty?
32
36
 
33
37
  content_type.split(SPLIT_PATTERN)[1..-1].each_with_object({}) do |s, hsh|
34
38
  s.strip!
@@ -40,9 +44,9 @@ module Rack
40
44
 
41
45
  private
42
46
 
43
- def strip_doublequotes(str)
44
- (str.start_with?('"') && str.end_with?('"')) ? str[1..-2] : str
45
- end
47
+ def strip_doublequotes(str)
48
+ (str && str.start_with?('"') && str.end_with?('"')) ? str[1..-2] : str || ''
49
+ end
46
50
  end
47
51
  end
48
52
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'cgi/cookie'
3
+ require 'stringio'
4
4
  require 'time'
5
5
 
6
6
  require_relative 'response'
@@ -11,6 +11,30 @@ module Rack
11
11
  # MockRequest.
12
12
 
13
13
  class MockResponse < Rack::Response
14
+ class Cookie
15
+ attr_reader :name, :value, :path, :domain, :expires, :secure
16
+
17
+ def initialize(args)
18
+ @name = args["name"]
19
+ @value = args["value"]
20
+ @path = args["path"]
21
+ @domain = args["domain"]
22
+ @expires = args["expires"]
23
+ @secure = args["secure"]
24
+ end
25
+
26
+ def method_missing(method_name, *args, &block)
27
+ @value.send(method_name, *args, &block)
28
+ end
29
+ # :nocov:
30
+ ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
31
+ # :nocov:
32
+
33
+ def respond_to_missing?(method_name, include_all = false)
34
+ @value.respond_to?(method_name, include_all) || super
35
+ end
36
+ end
37
+
14
38
  class << self
15
39
  alias [] new
16
40
  end
@@ -59,8 +83,16 @@ module Rack
59
83
  # end
60
84
  buffer = @buffered_body = String.new
61
85
 
62
- @body.each do |chunk|
63
- buffer << chunk
86
+ begin
87
+ if @body.respond_to?(:each)
88
+ @body.each do |chunk|
89
+ buffer << chunk
90
+ end
91
+ else
92
+ @body.call(StringIO.new(buffer))
93
+ end
94
+ ensure
95
+ @body.close if @body.respond_to?(:close)
64
96
  end
65
97
 
66
98
  return buffer
@@ -83,7 +115,7 @@ module Rack
83
115
  Array(set_cookie_header).each do |cookie|
84
116
  cookie_name, cookie_filling = cookie.split('=', 2)
85
117
  cookie_attributes = identify_cookie_attributes cookie_filling
86
- parsed_cookie = CGI::Cookie.new(
118
+ parsed_cookie = Cookie.new(
87
119
  'name' => cookie_name.strip,
88
120
  'value' => cookie_attributes.fetch('value'),
89
121
  'path' => cookie_attributes.fetch('path', nil),
@@ -100,7 +132,7 @@ module Rack
100
132
  def identify_cookie_attributes(cookie_filling)
101
133
  cookie_bits = cookie_filling.split(';')
102
134
  cookie_attributes = Hash.new
103
- cookie_attributes.store('value', cookie_bits[0].strip)
135
+ cookie_attributes.store('value', Array(cookie_bits[0].strip))
104
136
  cookie_bits.drop(1).each do |bit|
105
137
  if bit.include? '='
106
138
  cookie_attribute, attribute_value = bit.split('=', 2)
@@ -31,11 +31,25 @@ module Rack
31
31
  Error = BoundaryTooLongError
32
32
 
33
33
  EOL = "\r\n"
34
+ FWS = /[ \t]+(?:\r\n[ \t]+)?/ # whitespace with optional folding
35
+ HEADER_VALUE = "(?:[^\r\n]|\r\n[ \t])*" # anything but a non-folding CRLF
34
36
  MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni
35
- MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{EOL}/ni
36
- MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:(.*)(?=#{EOL}(\S|\z))/ni
37
- MULTIPART_CONTENT_ID = /Content-ID:\s*([^#{EOL}]*)/ni
38
-
37
+ MULTIPART_CONTENT_TYPE = /^Content-Type:#{FWS}?(#{HEADER_VALUE})/ni
38
+ MULTIPART_CONTENT_DISPOSITION = /^Content-Disposition:#{FWS}?(#{HEADER_VALUE})/ni
39
+ MULTIPART_CONTENT_ID = /^Content-ID:#{FWS}?(#{HEADER_VALUE})/ni
40
+
41
+ # Rack::Multipart::Parser handles parsing of multipart/form-data requests.
42
+ #
43
+ # File Parameter Contents
44
+ #
45
+ # When processing file uploads, the parser returns a hash containing
46
+ # information about uploaded files. For +file+ parameters, the hash includes:
47
+ #
48
+ # * +:filename+ - The original filename, already URL decoded by the parser
49
+ # * +:type+ - The content type of the uploaded file
50
+ # * +:name+ - The parameter name from the form
51
+ # * +:tempfile+ - A Tempfile object containing the uploaded data
52
+ # * +:head+ - The raw header content for this part
39
53
  class Parser
40
54
  BUFSIZE = 1_048_576
41
55
  TEXT_PLAIN = "text/plain"
@@ -45,6 +59,27 @@ module Rack
45
59
  Tempfile.new(["RackMultipart", extension])
46
60
  }
47
61
 
62
+ BOUNDARY_START_LIMIT = 16 * 1024
63
+ private_constant :BOUNDARY_START_LIMIT
64
+
65
+ MIME_HEADER_BYTESIZE_LIMIT = 64 * 1024
66
+ private_constant :MIME_HEADER_BYTESIZE_LIMIT
67
+
68
+ env_int = lambda do |key, val|
69
+ if str_val = ENV[key]
70
+ begin
71
+ val = Integer(str_val, 10)
72
+ rescue ArgumentError
73
+ raise ArgumentError, "non-integer value provided for environment variable #{key}"
74
+ end
75
+ end
76
+
77
+ val
78
+ end
79
+
80
+ BUFFERED_UPLOAD_BYTESIZE_LIMIT = env_int.call("RACK_MULTIPART_BUFFERED_UPLOAD_BYTESIZE_LIMIT", 16 * 1024 * 1024)
81
+ private_constant :BUFFERED_UPLOAD_BYTESIZE_LIMIT
82
+
48
83
  class BoundedIO # :nodoc:
49
84
  def initialize(io, content_length)
50
85
  @io = io
@@ -204,6 +239,8 @@ module Rack
204
239
 
205
240
  @state = :FAST_FORWARD
206
241
  @mime_index = 0
242
+ @body_retained = nil
243
+ @retained_size = 0
207
244
  @collector = Collector.new tempfile
208
245
 
209
246
  @sbuf = StringScanner.new("".dup)
@@ -241,7 +278,8 @@ module Rack
241
278
  @collector.each do |part|
242
279
  part.get_data do |data|
243
280
  tag_multipart_encoding(part.filename, part.content_type, part.name, data)
244
- @query_parser.normalize_params(@params, part.name, data)
281
+ name, data = handle_dummy_encoding(part.name, data)
282
+ @query_parser.normalize_params(@params, name, data)
245
283
  end
246
284
  end
247
285
  MultipartInfo.new @params.to_params_hash, @collector.find_all(&:file?).map(&:body)
@@ -249,12 +287,6 @@ module Rack
249
287
 
250
288
  private
251
289
 
252
- def dequote(str) # From WEBrick::HTTPUtils
253
- ret = (/\A"(.*)"\Z/ =~ str) ? $1 : str.dup
254
- ret.gsub!(/\\(.)/, "\\1")
255
- ret
256
- end
257
-
258
290
  def read_data(io, outbuf)
259
291
  content = io.read(@bufsize, outbuf)
260
292
  handle_empty_content!(content)
@@ -285,6 +317,10 @@ module Rack
285
317
 
286
318
  # retry for opening boundary
287
319
  else
320
+ # We raise if we don't find the multipart boundary, to avoid unbounded memory
321
+ # buffering. Note that the actual limit is the higher of 16KB and the buffer size (1MB by default)
322
+ raise Error, "multipart boundary not found within limit" if @sbuf.string.bytesize > BOUNDARY_START_LIMIT
323
+
288
324
  # no boundary found, keep reading data
289
325
  return :want_read
290
326
  end
@@ -401,16 +437,30 @@ module Rack
401
437
  name = filename || "#{content_type || TEXT_PLAIN}[]".dup
402
438
  end
403
439
 
440
+ # Mime part head data is retained for both TempfilePart and BufferPart
441
+ # for the entireity of the parse, even though it isn't used for BufferPart.
442
+ update_retained_size(head.bytesize)
443
+
444
+ # If a filename is given, a TempfilePart will be used, so the body will
445
+ # not be buffered in memory. However, if a filename is not given, a BufferPart
446
+ # will be used, and the body will be buffered in memory.
447
+ @body_retained = !filename
448
+
404
449
  @collector.on_mime_head @mime_index, head, filename, content_type, name
405
450
  @state = :MIME_BODY
406
451
  else
407
- :want_read
452
+ # We raise if the mime part header is too large, to avoid unbounded memory
453
+ # buffering. Note that the actual limit is the higher of 64KB and the buffer size (1MB by default)
454
+ raise Error, "multipart mime part header too large" if @sbuf.rest.bytesize > MIME_HEADER_BYTESIZE_LIMIT
455
+
456
+ return :want_read
408
457
  end
409
458
  end
410
459
 
411
460
  def handle_mime_body
412
461
  if (body_with_boundary = @sbuf.check_until(@body_regex)) # check but do not advance the pointer yet
413
462
  body = body_with_boundary.sub(@body_regex_at_end, '') # remove the boundary from the string
463
+ update_retained_size(body.bytesize) if @body_retained
414
464
  @collector.on_mime_body @mime_index, body
415
465
  @sbuf.pos += body.length + 2 # skip \r\n after the content
416
466
  @state = :CONSUME_TOKEN
@@ -419,7 +469,9 @@ module Rack
419
469
  # Save what we have so far
420
470
  if @rx_max_size < @sbuf.rest_size
421
471
  delta = @sbuf.rest_size - @rx_max_size
422
- @collector.on_mime_body @mime_index, @sbuf.peek(delta)
472
+ body = @sbuf.peek(delta)
473
+ update_retained_size(body.bytesize) if @body_retained
474
+ @collector.on_mime_body @mime_index, body
423
475
  @sbuf.pos += delta
424
476
  @sbuf.string = @sbuf.rest
425
477
  end
@@ -427,6 +479,13 @@ module Rack
427
479
  end
428
480
  end
429
481
 
482
+ def update_retained_size(size)
483
+ @retained_size += size
484
+ if @retained_size > BUFFERED_UPLOAD_BYTESIZE_LIMIT
485
+ raise Error, "multipart data over retained size limit"
486
+ end
487
+ end
488
+
430
489
  # Scan until the we find the start or end of the boundary.
431
490
  # If we find it, return the appropriate symbol for the start or
432
491
  # end of the boundary. If we don't find the start or end of the
@@ -492,6 +551,25 @@ module Rack
492
551
  Encoding::BINARY
493
552
  end
494
553
 
554
+ REENCODE_DUMMY_ENCODINGS = {
555
+ # ISO-2022-JP is a legacy but still widely used encoding in Japan
556
+ # Here we convert ISO-2022-JP to UTF-8 so that it can be handled.
557
+ Encoding::ISO_2022_JP => true
558
+
559
+ # Other dummy encodings are rarely used and have not been supported yet.
560
+ # Adding support for them will require careful considerations.
561
+ }
562
+
563
+ def handle_dummy_encoding(name, body)
564
+ # A string object with a 'dummy' encoding does not have full functionality and can cause errors.
565
+ # So here we covert it to UTF-8 so that it can be handled properly.
566
+ if name.encoding.dummy? && REENCODE_DUMMY_ENCODINGS[name.encoding]
567
+ name = name.encode(Encoding::UTF_8)
568
+ body = body.encode(Encoding::UTF_8)
569
+ end
570
+ return name, body
571
+ end
572
+
495
573
  def handle_empty_content!(content)
496
574
  if content.nil? || content.empty?
497
575
  raise EmptyContentError
@@ -5,14 +5,47 @@ require 'fileutils'
5
5
 
6
6
  module Rack
7
7
  module Multipart
8
+ # Despite the misleading name, UploadedFile is designed for use for
9
+ # preparing multipart file upload bodies, generally for use in tests.
10
+ # It is not designed for and should not be used for handling uploaded
11
+ # files (there is no need for that, since Rack's multipart parser
12
+ # already creates Tempfiles for that). Using this with non-trusted
13
+ # filenames can create a security vulnerability.
14
+ #
15
+ # You should only use this class if you plan on passing the instances
16
+ # to Rack::MockRequest for use in creating multipart request bodies.
17
+ #
18
+ # UploadedFile delegates most methods to the tempfile it contains.
8
19
  class UploadedFile
9
-
10
- # The filename, *not* including the path, of the "uploaded" file
20
+ # The provided name of the file. This generally is the basename of
21
+ # path provided during initialization, but it can contain slashes if they
22
+ # were present in the filename argument when the instance was created.
11
23
  attr_reader :original_filename
12
24
 
13
- # The content type of the "uploaded" file
25
+ # The content type of the instance.
14
26
  attr_accessor :content_type
15
27
 
28
+ # Create a new UploadedFile. For backwards compatibility, this accepts
29
+ # both positional and keyword versions of the same arguments:
30
+ #
31
+ # filepath/path :: The path to the file
32
+ # ct/content_type :: The content_type of the file
33
+ # bin/binary :: Whether to set binmode on the file before copying data into it.
34
+ #
35
+ # If both positional and keyword arguments are present, the keyword arguments
36
+ # take precedence.
37
+ #
38
+ # The following keyword-only arguments are also accepted:
39
+ #
40
+ # filename :: Override the filename to use for the file. This is so the
41
+ # filename for the upload does not need to match the basename of
42
+ # the file path. This should not contain slashes, unless you are
43
+ # trying to test how an application handles invalid filenames in
44
+ # multipart upload bodies.
45
+ # io :: Use the given IO-like instance as the tempfile, instead of creating
46
+ # a Tempfile instance. This is useful for building multipart file
47
+ # upload bodies without a file being present on the filesystem. If you are
48
+ # providing this, you should also provide the filename argument.
16
49
  def initialize(filepath = nil, ct = "text/plain", bin = false,
17
50
  path: filepath, content_type: ct, binary: bin, filename: nil, io: nil)
18
51
  if io
@@ -28,15 +61,19 @@ module Rack
28
61
  @content_type = content_type
29
62
  end
30
63
 
64
+ # The path of the tempfile for the instance, if the tempfile has a path.
65
+ # nil if the tempfile does not have a path.
31
66
  def path
32
67
  @tempfile.path if @tempfile.respond_to?(:path)
33
68
  end
34
69
  alias_method :local_path, :path
35
70
 
36
- def respond_to?(*args)
37
- super or @tempfile.respond_to?(*args)
71
+ # Return true if the tempfile responds to the method.
72
+ def respond_to_missing?(*args)
73
+ @tempfile.respond_to?(*args)
38
74
  end
39
75
 
76
+ # Delegate method missing calls to the tempfile.
40
77
  def method_missing(method_name, *args, &block) #:nodoc:
41
78
  @tempfile.__send__(method_name, *args, &block)
42
79
  end
@@ -21,21 +21,49 @@ module Rack
21
21
  include BadRequest
22
22
  end
23
23
 
24
- # ParamsTooDeepError is the error that is raised when params are recursively
25
- # nested over the specified limit.
26
- class ParamsTooDeepError < RangeError
24
+ # QueryLimitError is for errors raised when the query provided exceeds one
25
+ # of the query parser limits.
26
+ class QueryLimitError < RangeError
27
27
  include BadRequest
28
28
  end
29
29
 
30
- def self.make_default(param_depth_limit)
31
- new Params, param_depth_limit
30
+ # ParamsTooDeepError is the old name for the error that is raised when params
31
+ # are recursively nested over the specified limit. Make it the same as
32
+ # as QueryLimitError, so that code that rescues ParamsTooDeepError error
33
+ # to handle bad query strings also now handles other limits.
34
+ ParamsTooDeepError = QueryLimitError
35
+
36
+ def self.make_default(param_depth_limit, **options)
37
+ new(Params, param_depth_limit, **options)
32
38
  end
33
39
 
34
40
  attr_reader :param_depth_limit
35
41
 
36
- def initialize(params_class, param_depth_limit)
42
+ env_int = lambda do |key, val|
43
+ if str_val = ENV[key]
44
+ begin
45
+ val = Integer(str_val, 10)
46
+ rescue ArgumentError
47
+ raise ArgumentError, "non-integer value provided for environment variable #{key}"
48
+ end
49
+ end
50
+
51
+ val
52
+ end
53
+
54
+ BYTESIZE_LIMIT = env_int.call("RACK_QUERY_PARSER_BYTESIZE_LIMIT", 4194304)
55
+ private_constant :BYTESIZE_LIMIT
56
+
57
+ PARAMS_LIMIT = env_int.call("RACK_QUERY_PARSER_PARAMS_LIMIT", 4096)
58
+ private_constant :PARAMS_LIMIT
59
+
60
+ attr_reader :bytesize_limit
61
+
62
+ def initialize(params_class, param_depth_limit, bytesize_limit: BYTESIZE_LIMIT, params_limit: PARAMS_LIMIT)
37
63
  @params_class = params_class
38
64
  @param_depth_limit = param_depth_limit
65
+ @bytesize_limit = bytesize_limit
66
+ @params_limit = params_limit
39
67
  end
40
68
 
41
69
  # Stolen from Mongrel, with some small modifications:
@@ -43,14 +71,9 @@ module Rack
43
71
  # to parse cookies by changing the characters used in the second parameter
44
72
  # (which defaults to '&').
45
73
  def parse_query(qs, separator = nil, &unescaper)
46
- unescaper ||= method(:unescape)
47
-
48
74
  params = make_params
49
75
 
50
- (qs || '').split(separator ? (COMMON_SEP[separator] || /[#{separator}] */n) : DEFAULT_SEP).each do |p|
51
- next if p.empty?
52
- k, v = p.split('=', 2).map!(&unescaper)
53
-
76
+ each_query_pair(qs, separator, unescaper) do |k, v|
54
77
  if cur = params[k]
55
78
  if cur.class == Array
56
79
  params[k] << v
@@ -65,6 +88,19 @@ module Rack
65
88
  return params.to_h
66
89
  end
67
90
 
91
+ # Parses a query string by breaking it up at the '&', returning all key-value
92
+ # pairs as an array of [key, value] arrays. Unlike parse_query, this preserves
93
+ # all duplicate keys rather than collapsing them.
94
+ def parse_query_pairs(qs, separator = nil)
95
+ pairs = []
96
+
97
+ each_query_pair(qs, separator) do |k, v|
98
+ pairs << [k, v]
99
+ end
100
+
101
+ pairs
102
+ end
103
+
68
104
  # parse_nested_query expands a query string into structural types. Supported
69
105
  # types are Arrays, Hashes and basic value types. It is possible to supply
70
106
  # query strings with parameters of conflicting types, in this case a
@@ -73,17 +109,11 @@ module Rack
73
109
  def parse_nested_query(qs, separator = nil)
74
110
  params = make_params
75
111
 
76
- unless qs.nil? || qs.empty?
77
- (qs || '').split(separator ? (COMMON_SEP[separator] || /[#{separator}] */n) : DEFAULT_SEP).each do |p|
78
- k, v = p.split('=', 2).map! { |s| unescape(s) }
79
-
80
- _normalize_params(params, k, v, 0)
81
- end
112
+ each_query_pair(qs, separator) do |k, v|
113
+ _normalize_params(params, k, v, 0)
82
114
  end
83
115
 
84
116
  return params.to_h
85
- rescue ArgumentError => e
86
- raise InvalidParameterError, e.message, e.backtrace
87
117
  end
88
118
 
89
119
  # normalize_params recursively expands parameters into structural types. If
@@ -189,6 +219,37 @@ module Rack
189
219
  true
190
220
  end
191
221
 
222
+ def each_query_pair(qs, separator, unescaper = nil)
223
+ return if !qs || qs.empty?
224
+
225
+ if qs.bytesize > @bytesize_limit
226
+ raise QueryLimitError, "total query size exceeds limit (#{@bytesize_limit})"
227
+ end
228
+
229
+ pairs = qs.split(separator ? (COMMON_SEP[separator] || /[#{separator}] */n) : DEFAULT_SEP, @params_limit + 1)
230
+
231
+ if pairs.size > @params_limit
232
+ param_count = pairs.size + pairs.last.count(separator || "&")
233
+ raise QueryLimitError, "total number of query parameters (#{param_count}) exceeds limit (#{@params_limit})"
234
+ end
235
+
236
+ if unescaper
237
+ pairs.each do |p|
238
+ next if p.empty?
239
+ k, v = p.split('=', 2).map!(&unescaper)
240
+ yield k, v
241
+ end
242
+ else
243
+ pairs.each do |p|
244
+ next if p.empty?
245
+ k, v = p.split('=', 2).map! { |s| unescape(s) }
246
+ yield k, v
247
+ end
248
+ end
249
+ rescue ArgumentError => e
250
+ raise InvalidParameterError, e.message, e.backtrace
251
+ end
252
+
192
253
  def unescape(string, encoding = Encoding::UTF_8)
193
254
  URI.decode_www_form_component(string, encoding)
194
255
  end