rack 2.2.10 → 2.2.23

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 89c25f75ddd66c4ab311ec7afaad215f5417ab7c0478fb8da65bef7288b25ead
4
- data.tar.gz: 35bef5623220a06f4f1f54a752c0d5d67b23539d9521470446a323a03b25b3fa
3
+ metadata.gz: f1956bc102141711f91a9f0daa1507098bd8b7ede58febde9472b875dcb0d4e4
4
+ data.tar.gz: a8945afc610aed0c61ce8062e3b1394a24c6a457789637cc6835cfd881dac635
5
5
  SHA512:
6
- metadata.gz: 82b9812778a5e19704f7014a87b5dccffea6122e7a55295cfff5bfc2a581a11d718af4282b1503dc63bd16984ab286367da26ba533081dd7453c5c44d37a3616
7
- data.tar.gz: e7231940003fac65c76d7ed327e4f90d3b2ab31f665651e609a607e37952a43ced1b764429f052fef431e849f2bbc6a8841141ce4c51ac898d6bd34f54660b8e
6
+ metadata.gz: afe4c41e29d6112ba8fe825b33b93d10317d576add705a72de25825caa504c21eefe86cf9308df573dd422cfb4aed24e9453ad4e92720188f4f98c4d2f04c5c4
7
+ data.tar.gz: d28cd489644e2191917b28f39d689d384c21296e4f4a8a2bab7b1413f4110cb2092336039afc571d1278adbbfa4ebac9b2d27cb342b873336aa6efe037a93f4d
data/CHANGELOG.md CHANGED
@@ -2,7 +2,90 @@
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
- ## Unreleased
5
+ ## [2.2.23] - 2026-04-01
6
+
7
+ ### Security
8
+
9
+ - [CVE-2026-34763](https://github.com/advisories/GHSA-7mqq-6cf9-v2qp) Root directory disclosure via unescaped regex interpolation in `Rack::Directory`.
10
+ - [CVE-2026-34230](https://github.com/advisories/GHSA-v569-hp3g-36wr) Avoid O(n^2) algorithm in `Rack::Utils.select_best_encoding` which could lead to denial of service.
11
+ - [CVE-2026-26961](https://github.com/advisories/GHSA-vgpv-f759-9wx3) Raise error for multipart requests with multiple boundary parameters.
12
+ - [CVE-2026-34786](https://github.com/advisories/GHSA-q4qf-9j86-f5mh) `Rack::Static` `header_rules` bypass via URL-encoded path mismatch.
13
+ - [CVE-2026-34831](https://github.com/advisories/GHSA-q2ww-5357-x388) `Content-Length` mismatch in `Rack::Files` error responses.
14
+ - [CVE-2026-34826](https://github.com/advisories/GHSA-x8cg-fq8g-mxfx) Multipart byte range processing allows denial of service via excessive overlapping ranges.
15
+ - [CVE-2026-34830](https://github.com/advisories/GHSA-qv7j-4883-hwh7) `Rack::Sendfile` header-based `X-Accel-Mapping` regex injection enables unauthorized `X-Accel-Redirect`.
16
+ - [CVE-2026-34785](https://github.com/advisories/GHSA-h2jq-g4cq-5ppq) `Rack::Static` prefix matching can expose unintended files under the static root.
17
+ - [CVE-2026-34829](https://github.com/advisories/GHSA-8vqr-qjwx-82mw) Multipart parsing without `Content-Length` header allows unbounded chunked file uploads.
18
+
19
+ ## [2.2.22] - 2026-02-16
20
+
21
+ ### Security
22
+
23
+ - [CVE-2026-25500](https://github.com/advisories/GHSA-whrj-4476-wvmp) XSS injection via malicious filename in `Rack::Directory`.
24
+ - [CVE-2026-22860](https://github.com/advisories/GHSA-mxw3-3hh2-x2mh) Directory traversal via root prefix bypass in `Rack::Directory`.
25
+
26
+ ## [2.2.21] - 2025-11-03
27
+
28
+ ### Fixed
29
+
30
+ - Multipart parser: limit MIME header size check to the unread buffer region to avoid false `multipart mime part header too large` errors when previously read data accumulates in the scan buffer. ([#2392](https://github.com/rack/rack/pull/2392), [@alpaca-tc](https://github.com/alpaca-tc), [@willnet](https://github.com/willnet), [@krororo](https://github.com/krororo))
31
+
32
+ ## [2.2.20] - 2025-10-10
33
+
34
+ ### Security
35
+
36
+ - [CVE-2025-61780](https://github.com/advisories/GHSA-r657-rxjc-j557) Improper handling of headers in `Rack::Sendfile` may allow proxy bypass.
37
+ - [CVE-2025-61919](https://github.com/advisories/GHSA-6xw4-3v39-52mm) Unbounded read in `Rack::Request` form parsing can lead to memory exhaustion.
38
+
39
+ ## [2.2.19] - 2025-10-07
40
+
41
+ ### Security
42
+
43
+ - [CVE-2025-61772](https://github.com/advisories/GHSA-wpv5-97wm-hp9c) Multipart parser buffers unbounded per-part headers, enabling DoS (memory exhaustion)
44
+ - [CVE-2025-61771](https://github.com/advisories/GHSA-w9pc-fmgc-vxvw) Multipart parser buffers large non‑file fields entirely in memory, enabling DoS (memory exhaustion)
45
+ - [CVE-2025-61770](https://github.com/advisories/GHSA-p543-xpfm-54cp) Unbounded multipart preamble buffering enables DoS (memory exhaustion)
46
+
47
+ ## [2.2.18] - 2025-09-25
48
+
49
+ ### Security
50
+
51
+ - [CVE-2025-59830](https://github.com/advisories/GHSA-625h-95r8-8xpm) Unbounded parameter parsing in `Rack::QueryParser` can lead to memory exhaustion via semicolon-separated parameters.
52
+
53
+ ## [2.2.17] - 2025-06-03
54
+
55
+ - Backport `Rack::MediaType#params` now handles parameters without values. ([#2263](https://github.com/rack/rack/pull/2263), [@AllyMarthaJ](https://github.com/AllyMarthaJ))
56
+
57
+ ## [2.2.16] - 2025-05-22
58
+
59
+ - Fix incorrect backport of optional `CGI::Cookie` support. ([#2335](https://github.com/rack/rack/pull/2335), [@jeremyevans])
60
+
61
+ ## [2.2.15] - 2025-05-18
62
+
63
+ - Optional support for `CGI::Cookie` if not available. ([#2327](https://github.com/rack/rack/pull/2327), [#2333](https://github.com/rack/rack/pull/2333), [@earlopain])
64
+
65
+ ## [2.2.14] - 2025-05-06
66
+
67
+ ### Security
68
+
69
+ - [CVE-2025-32441](https://github.com/advisories/GHSA-vpfw-47h7-xj4g) Rack session can be restored after deletion.
70
+ - [CVE-2025-46727](https://github.com/advisories/GHSA-gjh7-p2fx-99vx) Unbounded parameter parsing in `Rack::QueryParser` can lead to memory exhaustion.
71
+
72
+ ## [2.2.13] - 2025-03-11
73
+
74
+ ### Security
75
+
76
+ - [CVE-2025-27610](https://github.com/advisories/GHSA-7wqh-767x-r66v) Local file inclusion in `Rack::Static`.
77
+
78
+ ## [2.2.12] - 2025-03-04
79
+
80
+ ### Security
81
+
82
+ - [CVE-2025-27111](https://github.com/advisories/GHSA-8cgq-6mh2-7j6v) Possible Log Injection in `Rack::Sendfile`.
83
+
84
+ ## [2.2.11] - 2025-02-12
85
+
86
+ ### Security
87
+
88
+ - [CVE-2025-25184](https://github.com/advisories/GHSA-7g2v-jj9q-g3rg) Possible Log Injection in `Rack::CommonLogger`.
6
89
 
7
90
  ## [2.2.10] - 2024-10-14
8
91
 
@@ -754,3 +837,7 @@ Items below this line are from the previously maintained HISTORY.md and NEWS.md
754
837
  - Removed Rails adapter, was too alpha.
755
838
 
756
839
  ## [0.1] 2007-03-03
840
+
841
+ [@ioquatix]: https://github.com/ioquatix "Samuel Williams"
842
+ [@jeremyevans]: https://github.com/jeremyevans "Jeremy Evans"
843
+ [@earlopain]: https://github.com/earlopain "Earlopain"
data/README.rdoc CHANGED
@@ -179,6 +179,41 @@ e.g:
179
179
 
180
180
  Rack::Utils.key_space_limit = 128
181
181
 
182
+ === `RACK_QUERY_PARSER_BYTESIZE_LIMIT`
183
+
184
+ This environment variable sets the default for the maximum query string bytesize
185
+ that `Rack::QueryParser` will attempt to parse. Attempts to use a query string
186
+ that exceeds this number of bytes will result in a
187
+ `Rack::QueryParser::QueryLimitError` exception. If this enviroment variable is
188
+ provided, it must be an integer, or `Rack::QueryParser` will raise an exception.
189
+
190
+ The default limit can be overridden on a per-`Rack::QueryParser` basis using
191
+ the `bytesize_limit` keyword argument when creating the `Rack::QueryParser`.
192
+
193
+ === `RACK_QUERY_PARSER_PARAMS_LIMIT`
194
+
195
+ This environment variable sets the default for the maximum number of query
196
+ parameters that `Rack::QueryParser` will attempt to parse. Attempts to use a
197
+ query string with more than this many query parameters will result in a
198
+ `Rack::QueryParser::QueryLimitError` exception. If this enviroment variable is
199
+ provided, it must be an integer, or `Rack::QueryParser` will raise an exception.
200
+
201
+ The default limit can be overridden on a per-`Rack::QueryParser` basis using
202
+ the `params_limit` keyword argument when creating the `Rack::QueryParser`.
203
+
204
+ This is implemented by counting the number of parameter separators in the
205
+ query string, before attempting parsing, so if the same parameter key is
206
+ used multiple times in the query, each counts as a separate parameter for
207
+ this check.
208
+
209
+ === `RACK_MULTIPART_BUFFERED_UPLOAD_BYTESIZE_LIMIT`
210
+
211
+ This environment variable sets the maximum amount of memory Rack will use
212
+ to buffer multipart parameters when parsing a request body. This considers
213
+ the size of the multipart mime headers and the body part for multipart
214
+ parameters that are buffered in memory and do not use tempfiles. This
215
+ defaults to 16MB if not provided.
216
+
182
217
  === key_space_limit
183
218
 
184
219
  The default number of bytes to allow all parameters keys in a given parameter hash to take up.
@@ -15,7 +15,7 @@ module Rack
15
15
  # The actual format is slightly different than the above due to the
16
16
  # separation of SCRIPT_NAME and PATH_INFO, and because the elapsed
17
17
  # time in seconds is included at the end.
18
- FORMAT = %{%s - %s [%s] "%s %s%s%s %s" %d %s %0.4f\n}
18
+ FORMAT = %{%s - %s [%s] "%s %s%s%s %s" %d %s %0.4f }
19
19
 
20
20
  # +logger+ can be any object that supports the +write+ or +<<+ methods,
21
21
  # which includes the standard library Logger. These methods are called
@@ -60,7 +60,8 @@ module Rack
60
60
  length,
61
61
  Utils.clock_time - began_at ]
62
62
 
63
- msg.gsub!(/[^[:print:]\n]/) { |c| "\\x#{c.ord}" }
63
+ msg.gsub!(/[^[:print:]]/) { |c| sprintf("\\x%x", c.ord) }
64
+ msg[-1] = "\n"
64
65
 
65
66
  logger = @logger || env[RACK_ERRORS]
66
67
 
@@ -11,7 +11,7 @@ module Rack
11
11
  # If +app+ is not specified, a Rack::Files of the same +root+ will be used.
12
12
 
13
13
  class Directory
14
- DIR_FILE = "<tr><td class='name'><a href='%s'>%s</a></td><td class='size'>%s</td><td class='type'>%s</td><td class='mtime'>%s</td></tr>\n"
14
+ DIR_FILE = "<tr><td class='name'><a href='./%s'>%s</a></td><td class='size'>%s</td><td class='type'>%s</td><td class='mtime'>%s</td></tr>\n"
15
15
  DIR_PAGE_HEADER = <<-PAGE
16
16
  <html><head>
17
17
  <title>%s</title>
@@ -45,7 +45,7 @@ table { width:100%%; }
45
45
  class DirectoryBody < Struct.new(:root, :path, :files)
46
46
  # Yield strings for each part of the directory entry
47
47
  def each
48
- show_path = Utils.escape_html(path.sub(/^#{root}/, ''))
48
+ show_path = Utils.escape_html(path.sub(/\A#{Regexp.escape(root)}/, ''))
49
49
  yield(DIR_PAGE_HEADER % [ show_path, show_path ])
50
50
 
51
51
  unless path.chomp('/') == root
@@ -76,6 +76,7 @@ table { width:100%%; }
76
76
  # Set the root directory and application for serving files.
77
77
  def initialize(root, app = nil)
78
78
  @root = ::File.expand_path(root)
79
+ @root_with_separator = @root.end_with?(::File::SEPARATOR) ? @root : "#{@root}#{::File::SEPARATOR}"
79
80
  @app = app || Files.new(@root)
80
81
  @head = Head.new(method(:get))
81
82
  end
@@ -112,7 +113,9 @@ table { width:100%%; }
112
113
  # Rack response to use for requests with paths outside the root, or nil if path is inside the root.
113
114
  def check_forbidden(path_info)
114
115
  return unless path_info.include? ".."
115
- return if ::File.expand_path(::File.join(@root, path_info)).start_with?(@root)
116
+
117
+ expanded_path = ::File.expand_path(::File.join(@root, path_info))
118
+ return if expanded_path == @root || expanded_path.start_with?(@root_with_separator)
116
119
 
117
120
  body = "Forbidden\n"
118
121
  [403, { CONTENT_TYPE => "text/plain",
data/lib/rack/files.rb CHANGED
@@ -196,7 +196,7 @@ EOF
196
196
  status,
197
197
  {
198
198
  CONTENT_TYPE => "text/plain",
199
- CONTENT_LENGTH => body.size.to_s,
199
+ CONTENT_LENGTH => body.bytesize.to_s,
200
200
  "X-Cascade" => "pass"
201
201
  }.merge!(headers),
202
202
  [body]
@@ -15,8 +15,6 @@ module Rack
15
15
  host = options.delete(:Host) || default_host
16
16
  port = options.delete(:Port) || 8080
17
17
  args = [host, port, app, options]
18
- # Thin versions below 0.8.0 do not support additional options
19
- args.pop if ::Thin::VERSION::MAJOR < 1 && ::Thin::VERSION::MINOR < 8
20
18
  server = ::Thin::Server.new(*args)
21
19
  yield server if block_given?
22
20
  server.start
@@ -27,6 +27,11 @@ module Rack
27
27
  # provided. e.g., when the CONTENT_TYPE is "text/plain;charset=utf-8",
28
28
  # this method responds with the following Hash:
29
29
  # { 'charset' => 'utf-8' }
30
+ #
31
+ # This will pass back parameters with empty strings in the hash if they
32
+ # lack a value (e.g., "text/plain;charset=" will return { 'charset' => '' },
33
+ # and "text/plain;charset" will return { 'charset' => '' }, similarly to
34
+ # the query params parser (barring the latter case, which returns nil instead)).
30
35
  def params(content_type)
31
36
  return {} if content_type.nil?
32
37
 
@@ -40,9 +45,9 @@ module Rack
40
45
 
41
46
  private
42
47
 
43
- def strip_doublequotes(str)
44
- (str.start_with?('"') && str.end_with?('"')) ? str[1..-2] : str
45
- end
48
+ def strip_doublequotes(str)
49
+ (str && str.start_with?('"') && str.end_with?('"')) ? str[1..-2] : str || ''
50
+ end
46
51
  end
47
52
  end
48
53
  end
data/lib/rack/mock.rb CHANGED
@@ -3,7 +3,6 @@
3
3
  require 'uri'
4
4
  require 'stringio'
5
5
  require_relative '../rack'
6
- require 'cgi/cookie'
7
6
 
8
7
  module Rack
9
8
  # Rack::MockRequest helps testing your Rack application without
@@ -171,6 +170,36 @@ module Rack
171
170
  # MockRequest.
172
171
 
173
172
  class MockResponse < Rack::Response
173
+ begin
174
+ # Recent versions of the CGI gem may not provide `CGI::Cookie`.
175
+ require 'cgi/cookie'
176
+ Cookie = CGI::Cookie
177
+ rescue LoadError
178
+ class Cookie
179
+ attr_reader :name, :value, :path, :domain, :expires, :secure
180
+
181
+ def initialize(args)
182
+ @name = args["name"]
183
+ @value = args["value"]
184
+ @path = args["path"]
185
+ @domain = args["domain"]
186
+ @expires = args["expires"]
187
+ @secure = args["secure"]
188
+ end
189
+
190
+ def method_missing(method_name, *args, &block)
191
+ @value.send(method_name, *args, &block)
192
+ end
193
+ # :nocov:
194
+ ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
195
+ # :nocov:
196
+
197
+ def respond_to_missing?(method_name, include_all = false)
198
+ @value.respond_to?(method_name, include_all) || super
199
+ end
200
+ end
201
+ end
202
+
174
203
  class << self
175
204
  alias [] new
176
205
  end
@@ -236,7 +265,7 @@ module Rack
236
265
  set_cookie_header.split("\n").each do |cookie|
237
266
  cookie_name, cookie_filling = cookie.split('=', 2)
238
267
  cookie_attributes = identify_cookie_attributes cookie_filling
239
- parsed_cookie = CGI::Cookie.new(
268
+ parsed_cookie = Cookie.new(
240
269
  'name' => cookie_name.strip,
241
270
  'value' => cookie_attributes.fetch('value'),
242
271
  'path' => cookie_attributes.fetch('path', nil),
@@ -253,7 +282,7 @@ module Rack
253
282
  def identify_cookie_attributes(cookie_filling)
254
283
  cookie_bits = cookie_filling.split(';')
255
284
  cookie_attributes = Hash.new
256
- cookie_attributes.store('value', cookie_bits[0].strip)
285
+ cookie_attributes.store('value', Array(cookie_bits[0].strip))
257
286
  cookie_bits.each do |bit|
258
287
  if bit.include? '='
259
288
  cookie_attribute, attribute_value = bit.split('=')
@@ -20,6 +20,31 @@ module Rack
20
20
 
21
21
  BOUNDARY_REGEX = /\A([^\n]*(?:\n|\Z))/
22
22
 
23
+ BOUNDARY_START_LIMIT = 16 * 1024
24
+ private_constant :BOUNDARY_START_LIMIT
25
+
26
+ MIME_HEADER_BYTESIZE_LIMIT = 64 * 1024
27
+ private_constant :MIME_HEADER_BYTESIZE_LIMIT
28
+
29
+ env_int = lambda do |key, val|
30
+ if str_val = ENV[key]
31
+ begin
32
+ val = Integer(str_val, 10)
33
+ rescue ArgumentError
34
+ raise ArgumentError, "non-integer value provided for environment variable #{key}"
35
+ end
36
+ end
37
+
38
+ val
39
+ end
40
+
41
+ BUFFERED_UPLOAD_BYTESIZE_LIMIT = env_int.call("RACK_MULTIPART_BUFFERED_UPLOAD_BYTESIZE_LIMIT", 16 * 1024 * 1024)
42
+ private_constant :BUFFERED_UPLOAD_BYTESIZE_LIMIT
43
+
44
+ bytesize_limit = env_int.call("RACK_MULTIPART_PARSER_BYTESIZE_LIMIT", 10 * 1024 * 1024 * 1024)
45
+ PARSER_BYTESIZE_LIMIT = bytesize_limit > 0 ? bytesize_limit : nil
46
+ private_constant :PARSER_BYTESIZE_LIMIT
47
+
23
48
  class BoundedIO # :nodoc:
24
49
  def initialize(io, content_length)
25
50
  @io = io
@@ -60,7 +85,15 @@ module Rack
60
85
  return unless content_type
61
86
  data = content_type.match(MULTIPART)
62
87
  return unless data
63
- data[1]
88
+
89
+ unless data[1].empty?
90
+ raise EOFError, "whitespace between boundary parameter name and equal sign"
91
+ end
92
+ if data.post_match =~ /boundary\s*=/i
93
+ raise EOFError, "multiple boundary parameters found in multipart content type"
94
+ end
95
+
96
+ data[2]
64
97
  end
65
98
 
66
99
  def self.parse(io, content_length, content_type, tmpfile, bufsize, qp)
@@ -69,6 +102,10 @@ module Rack
69
102
  boundary = parse_boundary content_type
70
103
  return EMPTY unless boundary
71
104
 
105
+ if PARSER_BYTESIZE_LIMIT && content_length && content_length > PARSER_BYTESIZE_LIMIT
106
+ raise EOFError, "multipart Content-Length #{content_length} exceeds limit of #{PARSER_BYTESIZE_LIMIT} bytes"
107
+ end
108
+
72
109
  io = BoundedIO.new(io, content_length) if content_length
73
110
  outbuf = String.new
74
111
 
@@ -187,6 +224,9 @@ module Rack
187
224
  @end_boundary = @boundary + '--'
188
225
  @state = :FAST_FORWARD
189
226
  @mime_index = 0
227
+ @body_retained = nil
228
+ @retained_size = 0
229
+ @total_bytes_read = (0 if PARSER_BYTESIZE_LIMIT)
190
230
  @collector = Collector.new tempfile
191
231
 
192
232
  @sbuf = StringScanner.new("".dup)
@@ -198,6 +238,12 @@ module Rack
198
238
 
199
239
  def on_read(content)
200
240
  handle_empty_content!(content)
241
+ if @total_bytes_read
242
+ @total_bytes_read += content.bytesize
243
+ if @total_bytes_read > PARSER_BYTESIZE_LIMIT
244
+ raise EOFError, "multipart upload exceeds limit of #{PARSER_BYTESIZE_LIMIT} bytes"
245
+ end
246
+ end
201
247
  @sbuf.concat content
202
248
  run_parser
203
249
  end
@@ -241,7 +287,13 @@ module Rack
241
287
  @state = :MIME_HEAD
242
288
  else
243
289
  raise EOFError, "bad content body" if @sbuf.rest_size >= @bufsize
244
- :want_read
290
+
291
+ # We raise if we don't find the multipart boundary, to avoid unbounded memory
292
+ # buffering. Note that the actual limit is the higher of 16KB and the buffer size (1MB by default)
293
+ raise EOFError, "multipart boundary not found within limit" if @sbuf.string.bytesize > BOUNDARY_START_LIMIT
294
+
295
+ # no boundary found, keep reading data
296
+ return :want_read
245
297
  end
246
298
  end
247
299
 
@@ -271,16 +323,30 @@ module Rack
271
323
  name = filename || "#{content_type || TEXT_PLAIN}[]".dup
272
324
  end
273
325
 
326
+ # Mime part head data is retained for both TempfilePart and BufferPart
327
+ # for the entireity of the parse, even though it isn't used for BufferPart.
328
+ update_retained_size(head.bytesize)
329
+
330
+ # If a filename is given, a TempfilePart will be used, so the body will
331
+ # not be buffered in memory. However, if a filename is not given, a BufferPart
332
+ # will be used, and the body will be buffered in memory.
333
+ @body_retained = !filename
334
+
274
335
  @collector.on_mime_head @mime_index, head, filename, content_type, name
275
336
  @state = :MIME_BODY
276
337
  else
277
- :want_read
338
+ # We raise if the mime part header is too large, to avoid unbounded memory
339
+ # buffering. Note that the actual limit is the higher of 64KB and the buffer size (1MB by default)
340
+ raise EOFError, "multipart mime part header too large" if @sbuf.rest.bytesize > MIME_HEADER_BYTESIZE_LIMIT
341
+
342
+ return :want_read
278
343
  end
279
344
  end
280
345
 
281
346
  def handle_mime_body
282
347
  if (body_with_boundary = @sbuf.check_until(@body_regex)) # check but do not advance the pointer yet
283
348
  body = body_with_boundary.sub(/#{@body_regex}\z/m, '') # remove the boundary from the string
349
+ update_retained_size(body.bytesize) if @body_retained
284
350
  @collector.on_mime_body @mime_index, body
285
351
  @sbuf.pos += body.length + 2 # skip \r\n after the content
286
352
  @state = :CONSUME_TOKEN
@@ -289,7 +355,9 @@ module Rack
289
355
  # Save what we have so far
290
356
  if @rx_max_size < @sbuf.rest_size
291
357
  delta = @sbuf.rest_size - @rx_max_size
292
- @collector.on_mime_body @mime_index, @sbuf.peek(delta)
358
+ body = @sbuf.peek(delta)
359
+ update_retained_size(body.bytesize) if @body_retained
360
+ @collector.on_mime_body @mime_index, body
293
361
  @sbuf.pos += delta
294
362
  @sbuf.string = @sbuf.rest
295
363
  end
@@ -299,6 +367,17 @@ module Rack
299
367
 
300
368
  def full_boundary; @full_boundary; end
301
369
 
370
+ def update_retained_size(size)
371
+ @retained_size += size
372
+ if @retained_size > BUFFERED_UPLOAD_BYTESIZE_LIMIT
373
+ raise EOFError, "multipart data over retained size limit"
374
+ end
375
+ end
376
+
377
+ # Scan until the we find the start or end of the boundary.
378
+ # If we find it, return the appropriate symbol for the start or
379
+ # end of the boundary. If we don't find the start or end of the
380
+ # boundary, clear the buffer and return nil.
302
381
  def consume_boundary
303
382
  while read_buffer = @sbuf.scan_until(BOUNDARY_REGEX)
304
383
  case read_buffer.strip
@@ -12,7 +12,7 @@ module Rack
12
12
 
13
13
  EOL = "\r\n"
14
14
  MULTIPART_BOUNDARY = "AaB03x"
15
- MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni
15
+ MULTIPART = %r|\Amultipart/.*?boundary(\s*)=\"?([^\";,]+)\"?|ni
16
16
  TOKEN = /[^\s()<>,;:\\"\/\[\]?=]+/
17
17
  CONDISP = /Content-Disposition:\s*#{TOKEN}\s*/i
18
18
  VALUE = /"(?:\\"|[^"])*"|#{TOKEN}/
@@ -16,20 +16,49 @@ module Rack
16
16
  # sequence.
17
17
  class InvalidParameterError < ArgumentError; end
18
18
 
19
- # ParamsTooDeepError is the error that is raised when params are recursively
20
- # nested over the specified limit.
21
- class ParamsTooDeepError < RangeError; end
19
+ # QueryLimitError is for errors raised when the query provided exceeds one
20
+ # of the query parser limits.
21
+ class QueryLimitError < RangeError
22
+ end
23
+
24
+ # ParamsTooDeepError is the old name for the error that is raised when params
25
+ # are recursively nested over the specified limit. Make it the same as
26
+ # as QueryLimitError, so that code that rescues ParamsTooDeepError error
27
+ # to handle bad query strings also now handles other limits.
28
+ ParamsTooDeepError = QueryLimitError
22
29
 
23
- def self.make_default(key_space_limit, param_depth_limit)
24
- new Params, key_space_limit, param_depth_limit
30
+ def self.make_default(key_space_limit, param_depth_limit, **options)
31
+ new(Params, key_space_limit, param_depth_limit, **options)
25
32
  end
26
33
 
27
34
  attr_reader :key_space_limit, :param_depth_limit
28
35
 
29
- def initialize(params_class, key_space_limit, param_depth_limit)
36
+ env_int = lambda do |key, val|
37
+ if str_val = ENV[key]
38
+ begin
39
+ val = Integer(str_val, 10)
40
+ rescue ArgumentError
41
+ raise ArgumentError, "non-integer value provided for environment variable #{key}"
42
+ end
43
+ end
44
+
45
+ val
46
+ end
47
+
48
+ BYTESIZE_LIMIT = env_int.call("RACK_QUERY_PARSER_BYTESIZE_LIMIT", 4194304)
49
+ private_constant :BYTESIZE_LIMIT
50
+
51
+ PARAMS_LIMIT = env_int.call("RACK_QUERY_PARSER_PARAMS_LIMIT", 4096)
52
+ private_constant :PARAMS_LIMIT
53
+
54
+ attr_reader :bytesize_limit
55
+
56
+ def initialize(params_class, key_space_limit, param_depth_limit, bytesize_limit: BYTESIZE_LIMIT, params_limit: PARAMS_LIMIT)
30
57
  @params_class = params_class
31
58
  @key_space_limit = key_space_limit
32
59
  @param_depth_limit = param_depth_limit
60
+ @bytesize_limit = bytesize_limit
61
+ @params_limit = params_limit
33
62
  end
34
63
 
35
64
  # Stolen from Mongrel, with some small modifications:
@@ -42,7 +71,7 @@ module Rack
42
71
 
43
72
  params = make_params
44
73
 
45
- (qs || '').split(d ? (COMMON_SEP[d] || /[#{d}] */n) : DEFAULT_SEP).each do |p|
74
+ check_query_string(qs, d).split(d ? (COMMON_SEP[d] || /[#{d}] */n) : DEFAULT_SEP).each do |p|
46
75
  next if p.empty?
47
76
  k, v = p.split('=', 2).map!(&unescaper)
48
77
 
@@ -69,7 +98,7 @@ module Rack
69
98
  params = make_params
70
99
 
71
100
  unless qs.nil? || qs.empty?
72
- (qs || '').split(d ? (COMMON_SEP[d] || /[#{d}] */n) : DEFAULT_SEP).each do |p|
101
+ check_query_string(qs, d).split(d ? (COMMON_SEP[d] || /[#{d}] */n) : DEFAULT_SEP).each do |p|
73
102
  k, v = p.split('=', 2).map! { |s| unescape(s) }
74
103
 
75
104
  normalize_params(params, k, v, param_depth_limit)
@@ -155,8 +184,24 @@ module Rack
155
184
  true
156
185
  end
157
186
 
158
- def unescape(s)
159
- Utils.unescape(s)
187
+ def check_query_string(qs, sep)
188
+ if qs
189
+ if qs.bytesize > @bytesize_limit
190
+ raise QueryLimitError, "total query size exceeds limit (#{@bytesize_limit})"
191
+ end
192
+
193
+ if (param_count = qs.count(sep.is_a?(String) ? sep : '&;')) >= @params_limit
194
+ raise QueryLimitError, "total number of query parameters (#{param_count+1}) exceeds limit (#{@params_limit})"
195
+ end
196
+
197
+ qs
198
+ else
199
+ ''
200
+ end
201
+ end
202
+
203
+ def unescape(string)
204
+ Utils.unescape(string)
160
205
  end
161
206
 
162
207
  class Params
data/lib/rack/request.rb CHANGED
@@ -444,7 +444,10 @@ module Rack
444
444
  get_header(RACK_REQUEST_FORM_HASH)
445
445
  elsif form_data? || parseable_data?
446
446
  unless set_header(RACK_REQUEST_FORM_HASH, parse_multipart)
447
- form_vars = get_header(RACK_INPUT).read
447
+ # Add 2 bytes. One to check whether it is over the limit, and a second
448
+ # in case the slice! call below removes the last byte
449
+ # If read returns nil, use the empty string
450
+ form_vars = get_header(RACK_INPUT).read(query_parser.bytesize_limit + 2) || ''
448
451
 
449
452
  # Fix for Safari Ajax postings that always append \0
450
453
  # form_vars.sub!(/\0\z/, '') # performance replacement:
data/lib/rack/sendfile.rb CHANGED
@@ -40,18 +40,23 @@ module Rack
40
40
  # proxy_set_header X-Real-IP $remote_addr;
41
41
  # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
42
42
  #
43
- # proxy_set_header X-Sendfile-Type X-Accel-Redirect;
44
43
  # proxy_set_header X-Accel-Mapping /var/www/=/files/;
45
44
  #
46
45
  # proxy_pass http://127.0.0.1:8080/;
47
46
  # }
48
47
  #
49
- # Note that the X-Sendfile-Type header must be set exactly as shown above.
50
48
  # The X-Accel-Mapping header should specify the location on the file system,
51
49
  # followed by an equals sign (=), followed name of the private URL pattern
52
- # that it maps to. The middleware performs a simple substitution on the
50
+ # that it maps to. The middleware performs a case-insensitive substitution on the
53
51
  # resulting path.
54
52
  #
53
+ # To enable X-Accel-Redirect, you must configure the middleware explicitly:
54
+ #
55
+ # use Rack::Sendfile, "X-Accel-Redirect"
56
+ #
57
+ # For security reasons, the X-Sendfile-Type header from requests is ignored.
58
+ # The sendfile variation must be set via the middleware constructor.
59
+ #
55
60
  # See Also: https://www.nginx.com/resources/wiki/start/topics/examples/xsendfile
56
61
  #
57
62
  # === lighttpd
@@ -96,13 +101,25 @@ module Rack
96
101
  # X-Accel-Mapping header. Mappings should be provided in tuples of internal to
97
102
  # external. The internal values may contain regular expression syntax, they
98
103
  # will be matched with case indifference.
104
+ #
105
+ # When X-Accel-Redirect is explicitly enabled via the variation parameter,
106
+ # and no application-level mappings are provided, the middleware will read
107
+ # the X-Accel-Mapping header from the proxy. This allows nginx to control
108
+ # the path mapping without requiring application-level configuration.
109
+ #
110
+ # === Security
111
+ #
112
+ # For security reasons, the X-Sendfile-Type header from HTTP requests is
113
+ # ignored. The sendfile variation must be explicitly configured via the
114
+ # middleware constructor to prevent information disclosure vulnerabilities
115
+ # where attackers could bypass proxy restrictions.
99
116
 
100
117
  class Sendfile
101
118
  def initialize(app, variation = nil, mappings = [])
102
119
  @app = app
103
120
  @variation = variation
104
121
  @mappings = mappings.map do |internal, external|
105
- [/^#{internal}/i, external]
122
+ [/\A#{internal}/i, external]
106
123
  end
107
124
  end
108
125
 
@@ -133,29 +150,42 @@ module Rack
133
150
  end
134
151
  when '', nil
135
152
  else
136
- env[RACK_ERRORS].puts "Unknown x-sendfile variation: '#{type}'.\n"
153
+ env[RACK_ERRORS].puts "Unknown x-sendfile variation: #{type.inspect}"
137
154
  end
138
155
  end
139
156
  [status, headers, body]
140
157
  end
141
158
 
142
159
  private
160
+
143
161
  def variation(env)
144
- @variation ||
145
- env['sendfile.type'] ||
146
- env['HTTP_X_SENDFILE_TYPE']
162
+ # Note: HTTP_X_SENDFILE_TYPE is intentionally NOT read for security reasons.
163
+ # Attackers could use this header to enable x-accel-redirect and bypass proxy restrictions.
164
+ @variation || env['sendfile.type']
165
+ end
166
+
167
+ def x_accel_mapping(env)
168
+ # Only allow header when:
169
+ # 1. X-Accel-Redirect is explicitly enabled via constructor.
170
+ # 2. No application-level mappings are configured.
171
+ return nil unless @variation =~ /x-accel-redirect/i
172
+ return nil if @mappings.any?
173
+
174
+ env['HTTP_X_ACCEL_MAPPING']
147
175
  end
148
176
 
149
177
  def map_accel_path(env, path)
150
178
  if mapping = @mappings.find { |internal, _| internal =~ path }
151
- path.sub(*mapping)
152
- elsif mapping = env['HTTP_X_ACCEL_MAPPING']
179
+ return path.sub(*mapping)
180
+ elsif mapping = x_accel_mapping(env)
181
+ # Safe to use header: explicit config + no app mappings:
153
182
  mapping.split(',').map(&:strip).each do |m|
154
183
  internal, external = m.split('=', 2).map(&:strip)
155
- new_path = path.sub(/^#{internal}/i, external)
184
+ new_path = path.sub(/\A#{Regexp.escape(internal)}/i, external)
156
185
  return new_path unless path == new_path
157
186
  end
158
- path
187
+
188
+ return path
159
189
  end
160
190
  end
161
191
  end
@@ -55,6 +55,7 @@ module Rack
55
55
 
56
56
  def write_session(req, session_id, new_session, options)
57
57
  with_lock(req) do
58
+ return false unless get_session_with_fallback(session_id)
58
59
  @pool.store session_id.private_id, new_session
59
60
  session_id
60
61
  end
@@ -64,7 +65,11 @@ module Rack
64
65
  with_lock(req) do
65
66
  @pool.delete(session_id.public_id)
66
67
  @pool.delete(session_id.private_id)
67
- generate_sid unless options[:drop]
68
+ unless options[:drop]
69
+ sid = generate_sid
70
+ @pool.store(sid.private_id, {})
71
+ sid
72
+ end
68
73
  end
69
74
  end
70
75
 
data/lib/rack/static.rb CHANGED
@@ -91,6 +91,9 @@ module Rack
91
91
  def initialize(app, options = {})
92
92
  @app = app
93
93
  @urls = options[:urls] || ["/favicon.ico"]
94
+ if @urls.kind_of?(Array)
95
+ @urls = @urls.map { |url| [url, url.end_with?('/') ? url : "#{url}/".freeze].freeze }.freeze
96
+ end
94
97
  @index = options[:index]
95
98
  @gzip = options[:gzip]
96
99
  @cascade = options[:cascade]
@@ -113,7 +116,7 @@ module Rack
113
116
  end
114
117
 
115
118
  def route_file(path)
116
- @urls.kind_of?(Array) && @urls.any? { |url| path.index(url) == 0 }
119
+ @urls.kind_of?(Array) && @urls.any? { |url, url_slash| path == url || path.start_with?(url_slash) }
117
120
  end
118
121
 
119
122
  def can_serve(path)
@@ -122,8 +125,9 @@ module Rack
122
125
 
123
126
  def call(env)
124
127
  path = env[PATH_INFO]
128
+ actual_path = Utils.clean_path_info(Utils.unescape_path(path))
125
129
 
126
- if can_serve(path)
130
+ if can_serve(actual_path)
127
131
  if overwrite_file_path(path)
128
132
  env[PATH_INFO] = (add_index_root?(path) ? path + @index : @urls[path])
129
133
  elsif @gzip && env['HTTP_ACCEPT_ENCODING'] && /\bgzip\b/.match?(env['HTTP_ACCEPT_ENCODING'])
@@ -164,6 +168,8 @@ module Rack
164
168
 
165
169
  # Convert HTTP header rules to HTTP headers
166
170
  def applicable_rules(path)
171
+ path = ::Rack::Utils.unescape_path(path)
172
+
167
173
  @header_rules.find_all do |rule, new_headers|
168
174
  case rule
169
175
  when :all
@@ -171,10 +177,9 @@ module Rack
171
177
  when :fonts
172
178
  /\.(?:ttf|otf|eot|woff2|woff|svg)\z/.match?(path)
173
179
  when String
174
- path = ::Rack::Utils.unescape(path)
175
180
  path.start_with?(rule) || path.start_with?('/' + rule)
176
181
  when Array
177
- /\.(#{rule.join('|')})\z/.match?(path)
182
+ /\.#{Regexp.union(rule)}\z/.match?(path)
178
183
  when Regexp
179
184
  rule.match?(path)
180
185
  else
data/lib/rack/utils.rb CHANGED
@@ -186,17 +186,41 @@ module Rack
186
186
  string.to_s.gsub(ESCAPE_HTML_PATTERN){|c| ESCAPE_HTML[c] }
187
187
  end
188
188
 
189
+ # Given an array of available encoding strings, and an array of
190
+ # acceptable encodings for a request, where each element of the
191
+ # acceptable encodings array is an array where the first element
192
+ # is an encoding name and the second element is the numeric
193
+ # priority for the encoding, return the available encoding with
194
+ # the highest priority.
195
+ #
196
+ # The accept_encoding argument is typically generated by calling
197
+ # Request#accept_encoding.
198
+ #
199
+ # Example:
200
+ #
201
+ # select_best_encoding(%w(compress gzip identity),
202
+ # [["compress", 0.5], ["gzip", 1.0]])
203
+ # # => "gzip"
204
+ #
205
+ # To reduce denial of service potential, only the first 16
206
+ # acceptable encodings are considered.
189
207
  def select_best_encoding(available_encodings, accept_encoding)
190
208
  # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
191
209
 
210
+ # Only process the first 16 encodings
211
+ accept_encoding = accept_encoding[0...16]
192
212
  expanded_accept_encoding = []
213
+ wildcard_seen = false
193
214
 
194
215
  accept_encoding.each do |m, q|
195
216
  preference = available_encodings.index(m) || available_encodings.size
196
217
 
197
218
  if m == "*"
198
- (available_encodings - accept_encoding.map(&:first)).each do |m2|
199
- expanded_accept_encoding << [m2, q, preference]
219
+ unless wildcard_seen
220
+ (available_encodings - accept_encoding.map(&:first)).each do |m2|
221
+ expanded_accept_encoding << [m2, q, preference]
222
+ end
223
+ wildcard_seen = true
200
224
  end
201
225
  else
202
226
  expanded_accept_encoding << [m, q, preference]
@@ -204,7 +228,13 @@ module Rack
204
228
  end
205
229
 
206
230
  encoding_candidates = expanded_accept_encoding
207
- .sort_by { |_, q, p| [-q, p] }
231
+ .sort do |(_, q1, p1), (_, q2, p2)|
232
+ if r = (q1 <=> q2).nonzero?
233
+ -r
234
+ else
235
+ (p1 <=> p2).nonzero? || 0
236
+ end
237
+ end
208
238
  .map!(&:first)
209
239
 
210
240
  unless encoding_candidates.include?("identity")
@@ -350,16 +380,17 @@ module Rack
350
380
  # Parses the "Range:" header, if present, into an array of Range objects.
351
381
  # Returns nil if the header is missing or syntactically invalid.
352
382
  # Returns an empty array if none of the ranges are satisfiable.
353
- def byte_ranges(env, size)
354
- warn "`byte_ranges` is deprecated, please use `get_byte_ranges`" if $VERBOSE
355
- get_byte_ranges env['HTTP_RANGE'], size
383
+ def byte_ranges(env, size, max_ranges: 100)
384
+ get_byte_ranges env['HTTP_RANGE'], size, max_ranges: max_ranges
356
385
  end
357
386
 
358
- def get_byte_ranges(http_range, size)
387
+ def get_byte_ranges(http_range, size, max_ranges: 100)
359
388
  # See <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35>
360
389
  return nil unless http_range && http_range =~ /bytes=([^;]+)/
390
+ byte_range = $1
391
+ return nil if byte_range.count(',') >= max_ranges
361
392
  ranges = []
362
- $1.split(/,\s*/).each do |range_spec|
393
+ byte_range.split(/,[ \t]*/).each do |range_spec|
363
394
  return nil unless range_spec.include?('-')
364
395
  range = range_spec.split('-')
365
396
  r0, r1 = range[0], range[1]
data/lib/rack/version.rb CHANGED
@@ -20,7 +20,7 @@ module Rack
20
20
  VERSION.join(".")
21
21
  end
22
22
 
23
- RELEASE = "2.2.10"
23
+ RELEASE = "2.2.23"
24
24
 
25
25
  # Return the Rack release as a dotted string.
26
26
  def self.release
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rack
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.10
4
+ version: 2.2.23
5
5
  platform: ruby
6
6
  authors:
7
7
  - Leah Neukirchen
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-10-14 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: minitest
@@ -77,9 +76,9 @@ executables:
77
76
  - rackup
78
77
  extensions: []
79
78
  extra_rdoc_files:
80
- - README.rdoc
81
79
  - CHANGELOG.md
82
80
  - CONTRIBUTING.md
81
+ - README.rdoc
83
82
  files:
84
83
  - CHANGELOG.md
85
84
  - CONTRIBUTING.md
@@ -169,7 +168,6 @@ metadata:
169
168
  changelog_uri: https://github.com/rack/rack/blob/master/CHANGELOG.md
170
169
  documentation_uri: https://rubydoc.info/github/rack/rack
171
170
  source_code_uri: https://github.com/rack/rack
172
- post_install_message:
173
171
  rdoc_options: []
174
172
  require_paths:
175
173
  - lib
@@ -184,8 +182,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
184
182
  - !ruby/object:Gem::Version
185
183
  version: '0'
186
184
  requirements: []
187
- rubygems_version: 3.5.11
188
- signing_key:
185
+ rubygems_version: 4.0.6
189
186
  specification_version: 4
190
187
  summary: A modular Ruby webserver interface.
191
188
  test_files: []