rack 3.1.21 → 3.2.0

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.

Potentially problematic release.


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

@@ -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
@@ -33,7 +32,7 @@ module Rack
33
32
  # and "text/plain;charset" will return { 'charset' => '' }, similarly to
34
33
  # the query params parser (barring the latter case, which returns nil instead)).
35
34
  def params(content_type)
36
- return {} if content_type.nil?
35
+ return {} if content_type.nil? || content_type.empty?
37
36
 
38
37
  content_type.split(SPLIT_PATTERN)[1..-1].each_with_object({}) do |s, hsh|
39
38
  s.strip!
@@ -10,33 +10,27 @@ module Rack
10
10
  # MockRequest.
11
11
 
12
12
  class MockResponse < Rack::Response
13
- begin
14
- # Recent versions of the CGI gem may not provide `CGI::Cookie`.
15
- require 'cgi/cookie'
16
- Cookie = CGI::Cookie
17
- rescue LoadError
18
- class Cookie
19
- attr_reader :name, :value, :path, :domain, :expires, :secure
20
-
21
- def initialize(args)
22
- @name = args["name"]
23
- @value = args["value"]
24
- @path = args["path"]
25
- @domain = args["domain"]
26
- @expires = args["expires"]
27
- @secure = args["secure"]
28
- end
13
+ class Cookie
14
+ attr_reader :name, :value, :path, :domain, :expires, :secure
15
+
16
+ def initialize(args)
17
+ @name = args["name"]
18
+ @value = args["value"]
19
+ @path = args["path"]
20
+ @domain = args["domain"]
21
+ @expires = args["expires"]
22
+ @secure = args["secure"]
23
+ end
29
24
 
30
- def method_missing(method_name, *args, &block)
31
- @value.send(method_name, *args, &block)
32
- end
33
- # :nocov:
34
- ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
35
- # :nocov:
25
+ def method_missing(method_name, *args, &block)
26
+ @value.send(method_name, *args, &block)
27
+ end
28
+ # :nocov:
29
+ ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
30
+ # :nocov:
36
31
 
37
- def respond_to_missing?(method_name, include_all = false)
38
- @value.respond_to?(method_name, include_all) || super
39
- end
32
+ def respond_to_missing?(method_name, include_all = false)
33
+ @value.respond_to?(method_name, include_all) || super
40
34
  end
41
35
  end
42
36
 
@@ -33,11 +33,23 @@ module Rack
33
33
  EOL = "\r\n"
34
34
  FWS = /[ \t]+(?:\r\n[ \t]+)?/ # whitespace with optional folding
35
35
  HEADER_VALUE = "(?:[^\r\n]|\r\n[ \t])*" # anything but a non-folding CRLF
36
- MULTIPART = %r|\Amultipart/.*?boundary(\s*)=\"?([^\";,]+)\"?|ni
36
+ MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni
37
37
  MULTIPART_CONTENT_TYPE = /^Content-Type:#{FWS}?(#{HEADER_VALUE})/ni
38
38
  MULTIPART_CONTENT_DISPOSITION = /^Content-Disposition:#{FWS}?(#{HEADER_VALUE})/ni
39
39
  MULTIPART_CONTENT_ID = /^Content-ID:#{FWS}?(#{HEADER_VALUE})/ni
40
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
41
53
  class Parser
42
54
  BUFSIZE = 1_048_576
43
55
  TEXT_PLAIN = "text/plain"
@@ -47,34 +59,6 @@ module Rack
47
59
  Tempfile.new(["RackMultipart", extension])
48
60
  }
49
61
 
50
- BOUNDARY_START_LIMIT = 16 * 1024
51
- private_constant :BOUNDARY_START_LIMIT
52
-
53
- MIME_HEADER_BYTESIZE_LIMIT = 64 * 1024
54
- private_constant :MIME_HEADER_BYTESIZE_LIMIT
55
-
56
- env_int = lambda do |key, val|
57
- if str_val = ENV[key]
58
- begin
59
- val = Integer(str_val, 10)
60
- rescue ArgumentError
61
- raise ArgumentError, "non-integer value provided for environment variable #{key}"
62
- end
63
- end
64
-
65
- val
66
- end
67
-
68
- BUFFERED_UPLOAD_BYTESIZE_LIMIT = env_int.call("RACK_MULTIPART_BUFFERED_UPLOAD_BYTESIZE_LIMIT", 16 * 1024 * 1024)
69
- private_constant :BUFFERED_UPLOAD_BYTESIZE_LIMIT
70
-
71
- bytesize_limit = env_int.call("RACK_MULTIPART_PARSER_BYTESIZE_LIMIT", 10 * 1024 * 1024 * 1024)
72
- PARSER_BYTESIZE_LIMIT = bytesize_limit > 0 ? bytesize_limit : nil
73
- private_constant :PARSER_BYTESIZE_LIMIT
74
-
75
- CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT = env_int.call("RACK_MULTIPART_CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT", 8 * 1024)
76
- private_constant :CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT
77
-
78
62
  class BoundedIO # :nodoc:
79
63
  def initialize(io, content_length)
80
64
  @io = io
@@ -111,15 +95,7 @@ module Rack
111
95
  return unless content_type
112
96
  data = content_type.match(MULTIPART)
113
97
  return unless data
114
-
115
- unless data[1].empty?
116
- raise Error, "whitespace between boundary parameter name and equal sign"
117
- end
118
- if data.post_match.match?(/boundary\s*=/i)
119
- raise BoundaryTooLongError, "multiple boundary parameters found in multipart content type"
120
- end
121
-
122
- data[2]
98
+ data[1]
123
99
  end
124
100
 
125
101
  def self.parse(io, content_length, content_type, tmpfile, bufsize, qp)
@@ -128,10 +104,6 @@ module Rack
128
104
  boundary = parse_boundary content_type
129
105
  return EMPTY unless boundary
130
106
 
131
- if PARSER_BYTESIZE_LIMIT && content_length && content_length > PARSER_BYTESIZE_LIMIT
132
- raise Error, "multipart Content-Length #{content_length} exceeds limit of #{PARSER_BYTESIZE_LIMIT} bytes"
133
- end
134
-
135
107
  if boundary.length > 70
136
108
  # RFC 1521 Section 7.2.1 imposes a 70 character maximum for the boundary.
137
109
  # Most clients use no more than 55 characters.
@@ -246,10 +218,6 @@ module Rack
246
218
 
247
219
  @state = :FAST_FORWARD
248
220
  @mime_index = 0
249
- @body_retained = nil
250
- @retained_size = 0
251
- @total_bytes_read = (0 if PARSER_BYTESIZE_LIMIT)
252
- @content_disposition_quoted_escapes = 0
253
221
  @collector = Collector.new tempfile
254
222
 
255
223
  @sbuf = StringScanner.new("".dup)
@@ -261,7 +229,6 @@ module Rack
261
229
  end
262
230
 
263
231
  def parse(io)
264
- @total_bytes_read &&= nil if io.is_a?(BoundedIO)
265
232
  outbuf = String.new
266
233
  read_data(io, outbuf)
267
234
 
@@ -288,7 +255,8 @@ module Rack
288
255
  @collector.each do |part|
289
256
  part.get_data do |data|
290
257
  tag_multipart_encoding(part.filename, part.content_type, part.name, data)
291
- @query_parser.normalize_params(@params, part.name, data)
258
+ name, data = handle_dummy_encoding(part.name, data)
259
+ @query_parser.normalize_params(@params, name, data)
292
260
  end
293
261
  end
294
262
  MultipartInfo.new @params.to_params_hash, @collector.find_all(&:file?).map(&:body)
@@ -296,21 +264,9 @@ module Rack
296
264
 
297
265
  private
298
266
 
299
- def dequote(str) # From WEBrick::HTTPUtils
300
- ret = (/\A"(.*)"\Z/ =~ str) ? $1 : str.dup
301
- ret.gsub!(/\\(.)/, "\\1")
302
- ret
303
- end
304
-
305
267
  def read_data(io, outbuf)
306
268
  content = io.read(@bufsize, outbuf)
307
269
  handle_empty_content!(content)
308
- if @total_bytes_read
309
- @total_bytes_read += content.bytesize
310
- if @total_bytes_read > PARSER_BYTESIZE_LIMIT
311
- raise Error, "multipart upload exceeds limit of #{PARSER_BYTESIZE_LIMIT} bytes"
312
- end
313
- end
314
270
  @sbuf.concat(content)
315
271
  end
316
272
 
@@ -338,10 +294,6 @@ module Rack
338
294
 
339
295
  # retry for opening boundary
340
296
  else
341
- # We raise if we don't find the multipart boundary, to avoid unbounded memory
342
- # buffering. Note that the actual limit is the higher of 16KB and the buffer size (1MB by default)
343
- raise Error, "multipart boundary not found within limit" if @sbuf.string.bytesize > BOUNDARY_START_LIMIT
344
-
345
297
  # no boundary found, keep reading data
346
298
  return :want_read
347
299
  end
@@ -404,11 +356,6 @@ module Rack
404
356
  # stop parsing parameter value if found ending quote
405
357
  break if c == '"'
406
358
 
407
- @content_disposition_quoted_escapes += 1
408
- if @content_disposition_quoted_escapes > CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT
409
- raise Error, "number of quoted escapes during content disposition parsing exceeds limit"
410
- end
411
-
412
359
  escaped_char = disposition.slice!(0, 1)
413
360
  if param == 'filename' && escaped_char != '"'
414
361
  # Possible IE uploaded filename, append both escape backslash and value
@@ -463,30 +410,16 @@ module Rack
463
410
  name = filename || "#{content_type || TEXT_PLAIN}[]".dup
464
411
  end
465
412
 
466
- # Mime part head data is retained for both TempfilePart and BufferPart
467
- # for the entireity of the parse, even though it isn't used for BufferPart.
468
- update_retained_size(head.bytesize)
469
-
470
- # If a filename is given, a TempfilePart will be used, so the body will
471
- # not be buffered in memory. However, if a filename is not given, a BufferPart
472
- # will be used, and the body will be buffered in memory.
473
- @body_retained = !filename
474
-
475
413
  @collector.on_mime_head @mime_index, head, filename, content_type, name
476
414
  @state = :MIME_BODY
477
415
  else
478
- # We raise if the mime part header is too large, to avoid unbounded memory
479
- # buffering. Note that the actual limit is the higher of 64KB and the buffer size (1MB by default)
480
- raise Error, "multipart mime part header too large" if @sbuf.rest.bytesize > MIME_HEADER_BYTESIZE_LIMIT
481
-
482
- return :want_read
416
+ :want_read
483
417
  end
484
418
  end
485
419
 
486
420
  def handle_mime_body
487
421
  if (body_with_boundary = @sbuf.check_until(@body_regex)) # check but do not advance the pointer yet
488
422
  body = body_with_boundary.sub(@body_regex_at_end, '') # remove the boundary from the string
489
- update_retained_size(body.bytesize) if @body_retained
490
423
  @collector.on_mime_body @mime_index, body
491
424
  @sbuf.pos += body.length + 2 # skip \r\n after the content
492
425
  @state = :CONSUME_TOKEN
@@ -495,9 +428,7 @@ module Rack
495
428
  # Save what we have so far
496
429
  if @rx_max_size < @sbuf.rest_size
497
430
  delta = @sbuf.rest_size - @rx_max_size
498
- body = @sbuf.peek(delta)
499
- update_retained_size(body.bytesize) if @body_retained
500
- @collector.on_mime_body @mime_index, body
431
+ @collector.on_mime_body @mime_index, @sbuf.peek(delta)
501
432
  @sbuf.pos += delta
502
433
  @sbuf.string = @sbuf.rest
503
434
  end
@@ -505,13 +436,6 @@ module Rack
505
436
  end
506
437
  end
507
438
 
508
- def update_retained_size(size)
509
- @retained_size += size
510
- if @retained_size > BUFFERED_UPLOAD_BYTESIZE_LIMIT
511
- raise Error, "multipart data over retained size limit"
512
- end
513
- end
514
-
515
439
  # Scan until the we find the start or end of the boundary.
516
440
  # If we find it, return the appropriate symbol for the start or
517
441
  # end of the boundary. If we don't find the start or end of the
@@ -577,6 +501,25 @@ module Rack
577
501
  Encoding::BINARY
578
502
  end
579
503
 
504
+ REENCODE_DUMMY_ENCODINGS = {
505
+ # ISO-2022-JP is a legacy but still widely used encoding in Japan
506
+ # Here we convert ISO-2022-JP to UTF-8 so that it can be handled.
507
+ Encoding::ISO_2022_JP => true
508
+
509
+ # Other dummy encodings are rarely used and have not been supported yet.
510
+ # Adding support for them will require careful considerations.
511
+ }
512
+
513
+ def handle_dummy_encoding(name, body)
514
+ # A string object with a 'dummy' encoding does not have full functionality and can cause errors.
515
+ # So here we covert it to UTF-8 so that it can be handled properly.
516
+ if name.encoding.dummy? && REENCODE_DUMMY_ENCODINGS[name.encoding]
517
+ name = name.encode(Encoding::UTF_8)
518
+ body = body.encode(Encoding::UTF_8)
519
+ end
520
+ return name, body
521
+ end
522
+
580
523
  def handle_empty_content!(content)
581
524
  if content.nil? || content.empty?
582
525
  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
@@ -57,8 +57,6 @@ module Rack
57
57
  PARAMS_LIMIT = env_int.call("RACK_QUERY_PARSER_PARAMS_LIMIT", 4096)
58
58
  private_constant :PARAMS_LIMIT
59
59
 
60
- attr_reader :bytesize_limit
61
-
62
60
  def initialize(params_class, param_depth_limit, bytesize_limit: BYTESIZE_LIMIT, params_limit: PARAMS_LIMIT)
63
61
  @params_class = params_class
64
62
  @param_depth_limit = param_depth_limit
@@ -71,14 +69,9 @@ module Rack
71
69
  # to parse cookies by changing the characters used in the second parameter
72
70
  # (which defaults to '&').
73
71
  def parse_query(qs, separator = nil, &unescaper)
74
- unescaper ||= method(:unescape)
75
-
76
72
  params = make_params
77
73
 
78
- check_query_string(qs, separator).split(separator ? (COMMON_SEP[separator] || /[#{separator}] */n) : DEFAULT_SEP).each do |p|
79
- next if p.empty?
80
- k, v = p.split('=', 2).map!(&unescaper)
81
-
74
+ each_query_pair(qs, separator, unescaper) do |k, v|
82
75
  if cur = params[k]
83
76
  if cur.class == Array
84
77
  params[k] << v
@@ -93,6 +86,19 @@ module Rack
93
86
  return params.to_h
94
87
  end
95
88
 
89
+ # Parses a query string by breaking it up at the '&', returning all key-value
90
+ # pairs as an array of [key, value] arrays. Unlike parse_query, this preserves
91
+ # all duplicate keys rather than collapsing them.
92
+ def parse_query_pairs(qs, separator = nil)
93
+ pairs = []
94
+
95
+ each_query_pair(qs, separator) do |k, v|
96
+ pairs << [k, v]
97
+ end
98
+
99
+ pairs
100
+ end
101
+
96
102
  # parse_nested_query expands a query string into structural types. Supported
97
103
  # types are Arrays, Hashes and basic value types. It is possible to supply
98
104
  # query strings with parameters of conflicting types, in this case a
@@ -101,17 +107,11 @@ module Rack
101
107
  def parse_nested_query(qs, separator = nil)
102
108
  params = make_params
103
109
 
104
- unless qs.nil? || qs.empty?
105
- check_query_string(qs, separator).split(separator ? (COMMON_SEP[separator] || /[#{separator}] */n) : DEFAULT_SEP).each do |p|
106
- k, v = p.split('=', 2).map! { |s| unescape(s) }
107
-
108
- _normalize_params(params, k, v, 0)
109
- end
110
+ each_query_pair(qs, separator) do |k, v|
111
+ _normalize_params(params, k, v, 0)
110
112
  end
111
113
 
112
114
  return params.to_h
113
- rescue ArgumentError => e
114
- raise InvalidParameterError, e.message, e.backtrace
115
115
  end
116
116
 
117
117
  # normalize_params recursively expands parameters into structural types. If
@@ -217,20 +217,35 @@ module Rack
217
217
  true
218
218
  end
219
219
 
220
- def check_query_string(qs, sep)
221
- if qs
222
- if qs.bytesize > @bytesize_limit
223
- raise QueryLimitError, "total query size exceeds limit (#{@bytesize_limit})"
224
- end
220
+ def each_query_pair(qs, separator, unescaper = nil)
221
+ return if !qs || qs.empty?
225
222
 
226
- if (param_count = qs.count(sep.is_a?(String) ? sep : '&')) >= @params_limit
227
- raise QueryLimitError, "total number of query parameters (#{param_count+1}) exceeds limit (#{@params_limit})"
228
- end
223
+ if qs.bytesize > @bytesize_limit
224
+ raise QueryLimitError, "total query size (#{qs.bytesize}) exceeds limit (#{@bytesize_limit})"
225
+ end
226
+
227
+ pairs = qs.split(separator ? (COMMON_SEP[separator] || /[#{separator}] */n) : DEFAULT_SEP, @params_limit + 1)
228
+
229
+ if pairs.size > @params_limit
230
+ param_count = pairs.size + pairs.last.count(separator || "&")
231
+ raise QueryLimitError, "total number of query parameters (#{param_count}) exceeds limit (#{@params_limit})"
232
+ end
229
233
 
230
- qs
234
+ if unescaper
235
+ pairs.each do |p|
236
+ next if p.empty?
237
+ k, v = p.split('=', 2).map!(&unescaper)
238
+ yield k, v
239
+ end
231
240
  else
232
- ''
241
+ pairs.each do |p|
242
+ next if p.empty?
243
+ k, v = p.split('=', 2).map! { |s| unescape(s) }
244
+ yield k, v
245
+ end
233
246
  end
247
+ rescue ArgumentError => e
248
+ raise InvalidParameterError, e.message, e.backtrace
234
249
  end
235
250
 
236
251
  def unescape(string, encoding = Encoding::UTF_8)