rack 3.0.4.1 → 3.0.5

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

Potentially problematic release.


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

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 966e37b13d3b25138d48a0a120a35b23f8821c95e55ae1132208dbf6e4d01f3e
4
- data.tar.gz: 849cf474ff1e7d79f4d1bc6a7b6396c174c21f6f2c5100d7b63030e9cc3fd808
3
+ metadata.gz: 14dbe09610211d4f7ea05476089ae7c5a276f62d267dea6f3902bdfbe8ecab6b
4
+ data.tar.gz: 12354e0b19330b88a4fe73e5675625b3ae096e6e2db087fc2e8d4167f4c10368
5
5
  SHA512:
6
- metadata.gz: 781ff34ba58e47c262f239af6d76b697a2d6df26329b21e5d055609c95b2855f13a9355d470c85f0055b6ff135edebdf83dba845452645a7a2965443b49bca8f
7
- data.tar.gz: 8a56368346afee246702533dfed7b444d2fab999bcc4d405484cb3af0b8e674ffa35509ddd402ed73b7a36bc9f9bb99dd09e2c31dafcb170c4cdbc63a095bd21
6
+ metadata.gz: eee29ac6d6b1f61355b1e802fece0f3d4af51603cfd0dca142ed7846f26059467a8cc08143f4dab9bfcb2fa20f93fd67d7654b23c9898b545761de0b1ea57003
7
+ data.tar.gz: f654a6cb49ed589ec844f930bc8dc0edec5c818665183011215b72c7b3dc7619b1726a2ec6ff045fd1914de85592c2697a8b10b45073744cba98124cc6f19dbb
data/CHANGELOG.md CHANGED
@@ -2,13 +2,17 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. For info on how to format all future additions to this file please reference [Keep A Changelog](https://keepachangelog.com/en/1.0.0/).
4
4
 
5
+ ## [3.0.4.1] - 2023-03-02
6
+
7
+ - [CVE-2023-27530] Introduce multipart_total_part_limit to limit total parts
8
+
5
9
  ## [3.0.4.1] - 2023-01-17
6
10
 
7
11
  - [CVE-2022-44571] Fix ReDoS vulnerability in multipart parser
8
12
  - [CVE-2022-44570] Fix ReDoS in Rack::Utils.get_byte_ranges
9
13
  - [CVE-2022-44572] Forbid control characters in attributes (also ReDoS)
10
14
 
11
- ## [3.0.4] - 2022-01-17
15
+ ## [3.0.4] - 2023-01-17
12
16
 
13
17
  - `Rack::Request#POST` should consistently raise errors. Cache errors that occur when invoking `Rack::Request#POST` so they can be raised again later. ([#2010](https://github.com/rack/rack/pull/2010), [@ioquatix])
14
18
  - Fix `Rack::Lint` error message for `HTTP_CONTENT_TYPE` and `HTTP_CONTENT_LENGTH`. ([#2007](https://github.com/rack/rack/pull/2007), [@byroot](https://github.com/byroot))
data/README.md CHANGED
@@ -186,19 +186,35 @@ but this query string would not be allowed:
186
186
 
187
187
  Limiting the depth prevents a possible stack overflow when parsing parameters.
188
188
 
189
- ### `multipart_part_limit`
189
+ ### `multipart_file_limit`
190
190
 
191
191
  ```ruby
192
- Rack::Utils.multipart_part_limit = 128 # default
192
+ Rack::Utils.multipart_file_limit = 128 # default
193
193
  ```
194
194
 
195
- The maximum number of parts a request can contain. Accepting too many parts can
196
- lead to the server running out of file handles.
195
+ The maximum number of parts with a filename a request can contain. Accepting
196
+ too many parts can lead to the server running out of file handles.
197
197
 
198
198
  The default is 128, which means that a single request can't upload more than 128
199
199
  files at once. Set to 0 for no limit.
200
200
 
201
- Can also be set via the `RACK_MULTIPART_PART_LIMIT` environment variable.
201
+ Can also be set via the `RACK_MULTIPART_FILE_LIMIT` environment variable.
202
+
203
+ (This is also aliased as `multipart_part_limit` and `RACK_MULTIPART_PART_LIMIT` for compatibility)
204
+
205
+
206
+ ### `multipart_total_part_limit`
207
+
208
+ The maximum total number of parts a request can contain of any type, including
209
+ both file and non-file form fields.
210
+
211
+ The default is 4096, which means that a single request can't contain more than
212
+ 4096 parts.
213
+
214
+ Set to 0 for no limit.
215
+
216
+ Can also be set via the `RACK_MULTIPART_TOTAL_PART_LIMIT` environment variable.
217
+
202
218
 
203
219
  ## Changelog
204
220
 
@@ -54,11 +54,13 @@ module Rack
54
54
  RACK_RESPONSE_FINISHED = 'rack.response_finished'
55
55
  RACK_REQUEST_FORM_INPUT = 'rack.request.form_input'
56
56
  RACK_REQUEST_FORM_HASH = 'rack.request.form_hash'
57
+ RACK_REQUEST_FORM_PAIRS = 'rack.request.form_pairs'
57
58
  RACK_REQUEST_FORM_VARS = 'rack.request.form_vars'
58
59
  RACK_REQUEST_FORM_ERROR = 'rack.request.form_error'
59
60
  RACK_REQUEST_COOKIE_HASH = 'rack.request.cookie_hash'
60
61
  RACK_REQUEST_COOKIE_STRING = 'rack.request.cookie_string'
61
62
  RACK_REQUEST_QUERY_HASH = 'rack.request.query_hash'
63
+ RACK_REQUEST_QUERY_PAIRS = 'rack.request.query_pairs'
62
64
  RACK_REQUEST_QUERY_STRING = 'rack.request.query_string'
63
65
  RACK_METHODOVERRIDE_ORIGINAL_METHOD = 'rack.methodoverride.original_method'
64
66
  end
@@ -8,6 +8,8 @@ module Rack
8
8
  module Multipart
9
9
  class MultipartPartLimitError < Errno::EMFILE; end
10
10
 
11
+ class MultipartTotalPartLimitError < StandardError; end
12
+
11
13
  # Use specific error class when parsing multipart request
12
14
  # that ends early.
13
15
  class EmptyContentError < ::EOFError; end
@@ -166,7 +168,7 @@ module Rack
166
168
 
167
169
  @mime_parts[mime_index] = klass.new(body, head, filename, content_type, name)
168
170
 
169
- check_open_files
171
+ check_part_limits
170
172
  end
171
173
 
172
174
  def on_mime_body(mime_index, content)
@@ -178,13 +180,23 @@ module Rack
178
180
 
179
181
  private
180
182
 
181
- def check_open_files
182
- if Utils.multipart_part_limit > 0
183
- if @open_files >= Utils.multipart_part_limit
183
+ def check_part_limits
184
+ file_limit = Utils.multipart_file_limit
185
+ part_limit = Utils.multipart_total_part_limit
186
+
187
+ if file_limit && file_limit > 0
188
+ if @open_files >= file_limit
184
189
  @mime_parts.each(&:close)
185
190
  raise MultipartPartLimitError, 'Maximum file multiparts in content reached'
186
191
  end
187
192
  end
193
+
194
+ if part_limit && part_limit > 0
195
+ if @mime_parts.size >= part_limit
196
+ @mime_parts.each(&:close)
197
+ raise MultipartTotalPartLimitError, 'Maximum total multiparts in content reached'
198
+ end
199
+ end
188
200
  end
189
201
  end
190
202
 
@@ -13,6 +13,31 @@ module Rack
13
13
  module Multipart
14
14
  MULTIPART_BOUNDARY = "AaB03x"
15
15
 
16
+ # Accumulator for multipart form data, conforming to the QueryParser API.
17
+ # In future, the Parser could return the pair list directly, but that would
18
+ # change its API.
19
+ class ParamList # :nodoc:
20
+ def self.make_params
21
+ new
22
+ end
23
+
24
+ def self.normalize_params(params, key, value)
25
+ params << [key, value]
26
+ end
27
+
28
+ def initialize
29
+ @pairs = []
30
+ end
31
+
32
+ def <<(pair)
33
+ @pairs << pair
34
+ end
35
+
36
+ def to_params_hash
37
+ @pairs
38
+ end
39
+ end
40
+
16
41
  class << self
17
42
  def parse_multipart(env, params = Rack::Utils.default_query_parser)
18
43
  io = env[RACK_INPUT]
@@ -37,19 +37,42 @@ module Rack
37
37
  @param_depth_limit = param_depth_limit
38
38
  end
39
39
 
40
- # Stolen from Mongrel, with some small modifications:
40
+ # Originally stolen from Mongrel, now with some modifications:
41
41
  # Parses a query string by breaking it up at the '&'. You can also use this
42
42
  # to parse cookies by changing the characters used in the second parameter
43
43
  # (which defaults to '&').
44
- def parse_query(qs, separator = nil, &unescaper)
45
- unescaper ||= method(:unescape)
44
+ #
45
+ # Returns an array of 2-element arrays, where the first element is the
46
+ # key and the second element is the value.
47
+ def split_query(qs, separator = nil, &unescaper)
48
+ pairs = []
49
+
50
+ if qs && !qs.empty?
51
+ unescaper ||= method(:unescape)
52
+
53
+ qs.split(separator ? (COMMON_SEP[separator] || /[#{separator}] */n) : DEFAULT_SEP).each do |p|
54
+ next if p.empty?
55
+ pair = p.split('=', 2).map!(&unescaper)
56
+ pair << nil if pair.length == 1
57
+ pairs << pair
58
+ end
59
+ end
46
60
 
47
- params = make_params
61
+ pairs
62
+ rescue ArgumentError => e
63
+ raise InvalidParameterError, e.message, e.backtrace
64
+ end
48
65
 
49
- (qs || '').split(separator ? (COMMON_SEP[separator] || /[#{separator}] */n) : DEFAULT_SEP).each do |p|
50
- next if p.empty?
51
- k, v = p.split('=', 2).map!(&unescaper)
66
+ # Parses a query string by breaking it up at the '&'. You can also use this
67
+ # to parse cookies by changing the characters used in the second parameter
68
+ # (which defaults to '&').
69
+ #
70
+ # Returns a hash where each value is a string (when a key only appears once)
71
+ # or an array of strings (when a key appears more than once).
72
+ def parse_query(qs, separator = nil, &unescaper)
73
+ params = make_params
52
74
 
75
+ split_query(qs, separator, &unescaper).each do |k, v|
53
76
  if cur = params[k]
54
77
  if cur.class == Array
55
78
  params[k] << v
@@ -61,7 +84,7 @@ module Rack
61
84
  end
62
85
  end
63
86
 
64
- return params.to_h
87
+ params.to_h
65
88
  end
66
89
 
67
90
  # parse_nested_query expands a query string into structural types. Supported
@@ -72,17 +95,11 @@ module Rack
72
95
  def parse_nested_query(qs, separator = nil)
73
96
  params = make_params
74
97
 
75
- unless qs.nil? || qs.empty?
76
- (qs || '').split(separator ? (COMMON_SEP[separator] || /[#{separator}] */n) : DEFAULT_SEP).each do |p|
77
- k, v = p.split('=', 2).map! { |s| unescape(s) }
78
-
79
- _normalize_params(params, k, v, 0)
80
- end
98
+ split_query(qs, separator).each do |k, v|
99
+ _normalize_params(params, k, v, 0)
81
100
  end
82
101
 
83
- return params.to_h
84
- rescue ArgumentError => e
85
- raise InvalidParameterError, e.message, e.backtrace
102
+ params.to_h
86
103
  end
87
104
 
88
105
  # normalize_params recursively expands parameters into structural types. If
data/lib/rack/request.rb CHANGED
@@ -483,11 +483,22 @@ module Rack
483
483
  # Returns the data received in the query string.
484
484
  def GET
485
485
  if get_header(RACK_REQUEST_QUERY_STRING) == query_string
486
- get_header(RACK_REQUEST_QUERY_HASH)
486
+ if query_hash = get_header(RACK_REQUEST_QUERY_HASH)
487
+ return query_hash
488
+ end
489
+ end
490
+
491
+ set_header(RACK_REQUEST_QUERY_HASH, expand_params(query_param_list))
492
+ end
493
+
494
+ def query_param_list
495
+ if get_header(RACK_REQUEST_QUERY_STRING) == query_string
496
+ get_header(RACK_REQUEST_QUERY_PAIRS)
487
497
  else
488
- query_hash = parse_query(query_string, '&')
489
- set_header(RACK_REQUEST_QUERY_STRING, query_string)
490
- set_header(RACK_REQUEST_QUERY_HASH, query_hash)
498
+ query_pairs = split_query(query_string, '&')
499
+ set_header RACK_REQUEST_QUERY_STRING, query_string
500
+ set_header RACK_REQUEST_QUERY_HASH, nil
501
+ set_header(RACK_REQUEST_QUERY_PAIRS, query_pairs)
491
502
  end
492
503
  end
493
504
 
@@ -496,32 +507,53 @@ module Rack
496
507
  # This method support both application/x-www-form-urlencoded and
497
508
  # multipart/form-data.
498
509
  def POST
510
+ if get_header(RACK_REQUEST_FORM_INPUT).equal?(get_header(RACK_INPUT))
511
+ if form_hash = get_header(RACK_REQUEST_FORM_HASH)
512
+ return form_hash
513
+ end
514
+ end
515
+
516
+ set_header(RACK_REQUEST_FORM_HASH, expand_params(body_param_list))
517
+ end
518
+
519
+ def body_param_list
499
520
  if error = get_header(RACK_REQUEST_FORM_ERROR)
500
521
  raise error.class, error.message, cause: error.cause
501
522
  end
502
523
 
503
524
  begin
504
- if get_header(RACK_INPUT).nil?
505
- raise "Missing rack.input"
506
- elsif get_header(RACK_REQUEST_FORM_INPUT) == get_header(RACK_INPUT)
507
- get_header(RACK_REQUEST_FORM_HASH)
525
+ rack_input = get_header(RACK_INPUT)
526
+
527
+ form_pairs = nil
528
+
529
+ # If the form data has already been memoized from the same
530
+ # input:
531
+ if get_header(RACK_REQUEST_FORM_INPUT).equal?(rack_input)
532
+ if form_pairs = get_header(RACK_REQUEST_FORM_PAIRS)
533
+ return form_pairs
534
+ end
535
+ end
536
+
537
+ if rack_input.nil?
538
+ form_pairs = []
508
539
  elsif form_data? || parseable_data?
509
- unless set_header(RACK_REQUEST_FORM_HASH, parse_multipart)
510
- form_vars = get_header(RACK_INPUT).read
540
+ unless form_pairs = Rack::Multipart.extract_multipart(self, Rack::Multipart::ParamList)
541
+ form_vars = rack_input.read
511
542
 
512
543
  # Fix for Safari Ajax postings that always append \0
513
544
  # form_vars.sub!(/\0\z/, '') # performance replacement:
514
545
  form_vars.slice!(-1) if form_vars.end_with?("\0")
515
546
 
516
547
  set_header RACK_REQUEST_FORM_VARS, form_vars
517
- set_header RACK_REQUEST_FORM_HASH, parse_query(form_vars, '&')
548
+ form_pairs = split_query(form_vars, '&')
518
549
  end
519
- set_header RACK_REQUEST_FORM_INPUT, get_header(RACK_INPUT)
520
- get_header RACK_REQUEST_FORM_HASH
521
550
  else
522
- set_header RACK_REQUEST_FORM_INPUT, get_header(RACK_INPUT)
523
- set_header(RACK_REQUEST_FORM_HASH, {})
551
+ form_pairs = []
524
552
  end
553
+
554
+ set_header RACK_REQUEST_FORM_INPUT, rack_input
555
+ set_header RACK_REQUEST_FORM_HASH, nil
556
+ set_header(RACK_REQUEST_FORM_PAIRS, form_pairs)
525
557
  rescue => error
526
558
  set_header(RACK_REQUEST_FORM_ERROR, error)
527
559
  raise
@@ -661,6 +693,28 @@ module Rack
661
693
  Rack::Multipart.extract_multipart(self, query_parser)
662
694
  end
663
695
 
696
+ def split_query(query, d = '&')
697
+ query_parser = query_parser()
698
+ unless query_parser.respond_to?(:split_query)
699
+ query_parser = Utils.default_query_parser
700
+ unless query_parser.respond_to?(:split_query)
701
+ query_parser = QueryParser.make_default(0)
702
+ end
703
+ end
704
+
705
+ query_parser.split_query(query, d)
706
+ end
707
+
708
+ def expand_params(pairs, query_parser = query_parser())
709
+ params = query_parser.make_params
710
+
711
+ pairs.each do |key, value|
712
+ query_parser.normalize_params(params, key, value)
713
+ end
714
+
715
+ params.to_params_hash
716
+ end
717
+
664
718
  def split_header(value)
665
719
  value ? value.strip.split(/[,\s]+/) : []
666
720
  end
data/lib/rack/utils.rb CHANGED
@@ -58,13 +58,24 @@ module Rack
58
58
  end
59
59
 
60
60
  class << self
61
- attr_accessor :multipart_part_limit
61
+ attr_accessor :multipart_total_part_limit
62
+
63
+ attr_accessor :multipart_file_limit
64
+
65
+ # multipart_part_limit is the original name of multipart_file_limit, but
66
+ # the limit only counts parts with filenames.
67
+ alias multipart_part_limit multipart_file_limit
68
+ alias multipart_part_limit= multipart_file_limit=
62
69
  end
63
70
 
64
- # The maximum number of parts a request can contain. Accepting too many part
65
- # can lead to the server running out of file handles.
71
+ # The maximum number of file parts a request can contain. Accepting too
72
+ # many parts can lead to the server running out of file handles.
66
73
  # Set to `0` for no limit.
67
- self.multipart_part_limit = (ENV['RACK_MULTIPART_PART_LIMIT'] || 128).to_i
74
+ self.multipart_file_limit = (ENV['RACK_MULTIPART_PART_LIMIT'] || ENV['RACK_MULTIPART_FILE_LIMIT'] || 128).to_i
75
+
76
+ # The maximum total number of parts a request can contain. Accepting too
77
+ # many can lead to excessive memory use and parsing time.
78
+ self.multipart_total_part_limit = (ENV['RACK_MULTIPART_TOTAL_PART_LIMIT'] || 4096).to_i
68
79
 
69
80
  def self.param_depth_limit
70
81
  default_query_parser.param_depth_limit
data/lib/rack/version.rb CHANGED
@@ -25,7 +25,7 @@ module Rack
25
25
  VERSION
26
26
  end
27
27
 
28
- RELEASE = "3.0.4.1"
28
+ RELEASE = "3.0.5"
29
29
 
30
30
  # Return the Rack release as a dotted string.
31
31
  def self.release
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rack
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.4.1
4
+ version: 3.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Leah Neukirchen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-01-17 00:00:00.000000000 Z
11
+ date: 2023-03-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest
@@ -164,7 +164,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
164
164
  - !ruby/object:Gem::Version
165
165
  version: '0'
166
166
  requirements: []
167
- rubygems_version: 3.1.6
167
+ rubygems_version: 3.4.6
168
168
  signing_key:
169
169
  specification_version: 4
170
170
  summary: A modular Ruby webserver interface.