rack 3.2.1 → 3.2.6

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: 32413bcd1f96d1cc6283491c3a7cf4b0301d9eb84d7d334895586d42981f0858
4
- data.tar.gz: ecd4aaba754ab7de09ced1dab1efb3366242933edeb1e549aaa14cdec1ff3cfc
3
+ metadata.gz: 37024c5110b365f1dd0b5627dd903edf42344fa9ad98a99af672e220d2370288
4
+ data.tar.gz: d0bd60323a75ff2828b33713963dcb5769898abc82a7ab3c2b9ac4ad36550b88
5
5
  SHA512:
6
- metadata.gz: bdf1806a0951c9523e75fe5a4be2eed0536c9e313b6f6808bb0ec7c43d62d489d63f75cb1e2a2a6741468d9fd2b34bb4ff22f0f8d32b5c3a0d3271faf2b11e73
7
- data.tar.gz: 98183f1e7637e3e9fa3a2b32908c8b00acdf294b526c413393eaff99daf6bf4e103c5501efe87fea54435747b7a782801e54dfbd89781513557ed56f047a1f1a
6
+ metadata.gz: 825e73fe75136333217b5f395f25245fe295fa4749a8068711011ba5c47350043941e4a4319837015e211f6cbd29952970fdc79f5deb0364c76d995c8d9de457
7
+ data.tar.gz: 57b8333f46704c6aead2d24ab214ab99ddb8296f93cc62298385d20bf4bffad829fae3b5e3a658e399d1628ab355a9abd69e4e1f2d08971740547b6ff1425f5b
data/CHANGELOG.md CHANGED
@@ -2,7 +2,55 @@
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
+ ## [3.2.6] - 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-32762](https://github.com/advisories/GHSA-qfgr-crr9-7r49) Forwarded header semicolon injection enables Host and Scheme spoofing.
12
+ - [CVE-2026-26961](https://github.com/advisories/GHSA-vgpv-f759-9wx3) Raise error for multipart requests with multiple boundary parameters.
13
+ - [CVE-2026-34786](https://github.com/advisories/GHSA-q4qf-9j86-f5mh) `Rack::Static` `header_rules` bypass via URL-encoded path mismatch.
14
+ - [CVE-2026-34831](https://github.com/advisories/GHSA-q2ww-5357-x388) `Content-Length` mismatch in `Rack::Files` error responses.
15
+ - [CVE-2026-34826](https://github.com/advisories/GHSA-x8cg-fq8g-mxfx) Multipart byte range processing allows denial of service via excessive overlapping ranges.
16
+ - [CVE-2026-34835](https://github.com/advisories/GHSA-g2pf-xv49-m2h5) `Rack::Request` accepts invalid Host characters, enabling host allowlist bypass.
17
+ - [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`.
18
+ - [CVE-2026-34785](https://github.com/advisories/GHSA-h2jq-g4cq-5ppq) `Rack::Static` prefix matching can expose unintended files under the static root.
19
+ - [CVE-2026-34829](https://github.com/advisories/GHSA-8vqr-qjwx-82mw) Multipart parsing without `Content-Length` header allows unbounded chunked file uploads.
20
+ - [CVE-2026-34827](https://github.com/advisories/GHSA-v6x5-cg8r-vv6x) Quadratic-time multipart header parsing allows denial of service via escape-heavy quoted parameters.
21
+ - [CVE-2026-26962](https://github.com/advisories/GHSA-rx22-g9mx-qrhv) Improper unfolding of folded multipart headers preserves CRLF in parsed parameter values.
22
+
23
+ ## [3.2.5] - 2026-02-16
24
+
25
+ ### Security
26
+
27
+ - [CVE-2026-25500](https://github.com/advisories/GHSA-whrj-4476-wvmp) XSS injection via malicious filename in `Rack::Directory`.
28
+ - [CVE-2026-22860](https://github.com/advisories/GHSA-mxw3-3hh2-x2mh) Directory traversal via root prefix bypass in `Rack::Directory`.
29
+
30
+ ### Fixed
31
+
32
+ - Fix `Rack::MockResponse#body` when the body is a Proc. ([#2420](https://github.com/rack/rack/pull/2420), [#2423](https://github.com/rack/rack/pull/2423), [@tavianator](https://github.com/tavianator), [@ioquatix])
33
+
34
+ ## [3.2.4] - 2025-11-03
35
+
36
+ ### Fixed
37
+
38
+ - 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))
39
+
40
+ ## [3.2.3] - 2025-10-10
41
+
42
+ ### Security
43
+
44
+ - [CVE-2025-61780](https://github.com/advisories/GHSA-r657-rxjc-j557) Improper handling of headers in `Rack::Sendfile` may allow proxy bypass.
45
+ - [CVE-2025-61919](https://github.com/advisories/GHSA-6xw4-3v39-52mm) Unbounded read in `Rack::Request` form parsing can lead to memory exhaustion.
46
+
47
+ ## [3.2.2] - 2025-10-07
48
+
49
+ ### Security
50
+
51
+ - [CVE-2025-61772](https://github.com/advisories/GHSA-wpv5-97wm-hp9c) Multipart parser buffers unbounded per-part headers, enabling DoS (memory exhaustion)
52
+ - [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)
53
+ - [CVE-2025-61770](https://github.com/advisories/GHSA-p543-xpfm-54cp) Unbounded multipart preamble buffering enables DoS (memory exhaustion)
6
54
 
7
55
  ## [3.2.1] -- 2025-09-02
8
56
 
@@ -61,6 +109,34 @@ This release continues Rack's evolution toward a cleaner, more efficient foundat
61
109
  - `SERVER_NAME` and `HTTP_HOST` are now more strictly validated according to the relevant specifications. ([#2298](https://github.com/rack/rack/pull/2298), [@ioquatix])
62
110
  - `Rack::Lint` now disallows `PATH_INFO="" SCRIPT_NAME=""`. ([#2298](https://github.com/rack/rack/issues/2307), [@jeremyevans])
63
111
 
112
+ ## [3.1.20] - 2026-02-16
113
+
114
+ ### Security
115
+
116
+ - [CVE-2026-25500](https://github.com/advisories/GHSA-whrj-4476-wvmp) XSS injection via malicious filename in `Rack::Directory`.
117
+ - [CVE-2026-22860](https://github.com/advisories/GHSA-mxw3-3hh2-x2mh) Directory traversal via root prefix bypass in `Rack::Directory`.
118
+
119
+ ## [3.1.19] - 2025-11-03
120
+
121
+ ### Fixed
122
+
123
+ - 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))
124
+
125
+ ## [3.1.18] - 2025-10-10
126
+
127
+ ### Security
128
+
129
+ - [CVE-2025-61780](https://github.com/advisories/GHSA-r657-rxjc-j557) Improper handling of headers in `Rack::Sendfile` may allow proxy bypass.
130
+ - [CVE-2025-61919](https://github.com/advisories/GHSA-6xw4-3v39-52mm) Unbounded read in `Rack::Request` form parsing can lead to memory exhaustion.
131
+
132
+ ## [3.1.17] - 2025-10-07
133
+
134
+ ### Security
135
+
136
+ - [CVE-2025-61772](https://github.com/advisories/GHSA-wpv5-97wm-hp9c) Multipart parser buffers unbounded per-part headers, enabling DoS (memory exhaustion)
137
+ - [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)
138
+ - [CVE-2025-61770](https://github.com/advisories/GHSA-p543-xpfm-54cp) Unbounded multipart preamble buffering enables DoS (memory exhaustion)
139
+
64
140
  ## [3.1.16] - 2025-06-04
65
141
 
66
142
  ### Security
@@ -77,7 +153,7 @@ This release continues Rack's evolution toward a cleaner, more efficient foundat
77
153
 
78
154
  ### Security
79
155
 
80
- - [CVE-2025-46727](https://github.com/rack/rack/security/advisories/GHSA-gjh7-p2fx-99vx) Unbounded parameter parsing in `Rack::QueryParser` can lead to memory exhaustion.
156
+ - [CVE-2025-46727](https://github.com/advisories/GHSA-gjh7-p2fx-99vx) Unbounded parameter parsing in `Rack::QueryParser` can lead to memory exhaustion.
81
157
 
82
158
  ## [3.1.13] - 2025-04-13
83
159
 
@@ -87,19 +163,19 @@ This release continues Rack's evolution toward a cleaner, more efficient foundat
87
163
 
88
164
  ### Security
89
165
 
90
- - [CVE-2025-27610](https://github.com/rack/rack/security/advisories/GHSA-7wqh-767x-r66v) Local file inclusion in `Rack::Static`.
166
+ - [CVE-2025-27610](https://github.com/advisories/GHSA-7wqh-767x-r66v) Local file inclusion in `Rack::Static`.
91
167
 
92
168
  ## [3.1.11] - 2025-03-04
93
169
 
94
170
  ### Security
95
171
 
96
- - [CVE-2025-27111](https://github.com/rack/rack/security/advisories/GHSA-8cgq-6mh2-7j6v) Possible Log Injection in `Rack::Sendfile`.
172
+ - [CVE-2025-27111](https://github.com/advisories/GHSA-8cgq-6mh2-7j6v) Possible Log Injection in `Rack::Sendfile`.
97
173
 
98
174
  ## [3.1.10] - 2025-02-12
99
175
 
100
176
  ### Security
101
177
 
102
- - [CVE-2025-25184](https://github.com/rack/rack/security/advisories/GHSA-7g2v-jj9q-g3rg) Possible Log Injection in `Rack::CommonLogger`.
178
+ - [CVE-2025-25184](https://github.com/advisories/GHSA-7g2v-jj9q-g3rg) Possible Log Injection in `Rack::CommonLogger`.
103
179
 
104
180
  ## [3.1.9] - 2025-01-31
105
181
 
@@ -132,7 +208,7 @@ This release continues Rack's evolution toward a cleaner, more efficient foundat
132
208
 
133
209
  ### Security
134
210
 
135
- - Fix potential ReDoS attack in `Rack::Request#parse_http_accept_header`. ([GHSA-cj83-2ww7-mvq7](https://github.com/rack/rack/security/advisories/GHSA-cj83-2ww7-mvq7), [@dwisiswant0](https://github.com/dwisiswant0))
211
+ - Fix potential ReDoS attack in `Rack::Request#parse_http_accept_header`. ([GHSA-cj83-2ww7-mvq7](https://github.com/advisories/GHSA-cj83-2ww7-mvq7), [@dwisiswant0](https://github.com/dwisiswant0))
136
212
 
137
213
  ## [3.1.4] - 2024-06-22
138
214
 
@@ -224,7 +300,7 @@ This release is primarily a maintenance release that removes features deprecated
224
300
 
225
301
  ### Security
226
302
 
227
- - [CVE-2025-46727](https://github.com/rack/rack/security/advisories/GHSA-gjh7-p2fx-99vx) Unbounded parameter parsing in `Rack::QueryParser` can lead to memory exhaustion.
303
+ - [CVE-2025-46727](https://github.com/advisories/GHSA-gjh7-p2fx-99vx) Unbounded parameter parsing in `Rack::QueryParser` can lead to memory exhaustion.
228
304
 
229
305
  ## [3.0.15] - 2025-04-13
230
306
 
@@ -234,13 +310,13 @@ This release is primarily a maintenance release that removes features deprecated
234
310
 
235
311
  ### Security
236
312
 
237
- - [CVE-2025-27610](https://github.com/rack/rack/security/advisories/GHSA-7wqh-767x-r66v) Local file inclusion in `Rack::Static`.
313
+ - [CVE-2025-27610](https://github.com/advisories/GHSA-7wqh-767x-r66v) Local file inclusion in `Rack::Static`.
238
314
 
239
315
  ## [3.0.13] - 2025-03-04
240
316
 
241
317
  ### Security
242
318
 
243
- - [CVE-2025-27111](https://github.com/rack/rack/security/advisories/GHSA-8cgq-6mh2-7j6v) Possible Log Injection in `Rack::Sendfile`.
319
+ - [CVE-2025-27111](https://github.com/advisories/GHSA-8cgq-6mh2-7j6v) Possible Log Injection in `Rack::Sendfile`.
244
320
 
245
321
  ### Fixed
246
322
 
@@ -250,7 +326,7 @@ This release is primarily a maintenance release that removes features deprecated
250
326
 
251
327
  ### Security
252
328
 
253
- - [CVE-2025-25184](https://github.com/rack/rack/security/advisories/GHSA-7g2v-jj9q-g3rg) Possible Log Injection in `Rack::CommonLogger`.
329
+ - [CVE-2025-25184](https://github.com/advisories/GHSA-7g2v-jj9q-g3rg) Possible Log Injection in `Rack::CommonLogger`.
254
330
 
255
331
  ## [3.0.11] - 2024-05-10
256
332
 
@@ -430,6 +506,40 @@ This release introduces major improvements to Rack, including enhanced support f
430
506
  - Fix multipart filename generation for filenames that contain spaces. Encode spaces as "%20" instead of "+" which will be decoded properly by the multipart parser. ([#1736](https://github.com/rack/rack/pull/1645), [@muirdm](https://github.com/muirdm))
431
507
  - `Rack::Request#scheme` returns `ws` or `wss` when one of the `X-Forwarded-Scheme` / `X-Forwarded-Proto` headers is set to `ws` or `wss`, respectively. ([#1730](https://github.com/rack/rack/issues/1730), [@erwanst](https://github.com/erwanst))
432
508
 
509
+ ## [2.2.22] - 2026-02-16
510
+
511
+ ### Security
512
+
513
+ - [CVE-2026-25500](https://github.com/advisories/GHSA-whrj-4476-wvmp) XSS injection via malicious filename in `Rack::Directory`.
514
+ - [CVE-2026-22860](https://github.com/advisories/GHSA-mxw3-3hh2-x2mh) Directory traversal via root prefix bypass in `Rack::Directory`.
515
+
516
+ ## [2.2.21] - 2025-11-03
517
+
518
+ ### Fixed
519
+
520
+ - 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))
521
+
522
+ ## [2.2.20] - 2025-10-10
523
+
524
+ ### Security
525
+
526
+ - [CVE-2025-61780](https://github.com/advisories/GHSA-r657-rxjc-j557) Improper handling of headers in `Rack::Sendfile` may allow proxy bypass.
527
+ - [CVE-2025-61919](https://github.com/advisories/GHSA-6xw4-3v39-52mm) Unbounded read in `Rack::Request` form parsing can lead to memory exhaustion.
528
+
529
+ ## [2.2.19] - 2025-10-07
530
+
531
+ ### Security
532
+
533
+ - [CVE-2025-61772](https://github.com/advisories/GHSA-wpv5-97wm-hp9c) Multipart parser buffers unbounded per-part headers, enabling DoS (memory exhaustion)
534
+ - [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)
535
+ - [CVE-2025-61770](https://github.com/advisories/GHSA-p543-xpfm-54cp) Unbounded multipart preamble buffering enables DoS (memory exhaustion)
536
+
537
+ ## [2.2.18] - 2025-09-25
538
+
539
+ ### Security
540
+
541
+ - [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.
542
+
433
543
  ## [2.2.17] - 2025-06-03
434
544
 
435
545
  - Backport `Rack::MediaType#params` now handles parameters without values. ([#2263](https://github.com/rack/rack/pull/2263), [@AllyMarthaJ](https://github.com/AllyMarthaJ))
@@ -448,25 +558,25 @@ This release introduces major improvements to Rack, including enhanced support f
448
558
 
449
559
  ### Security
450
560
 
451
- - [CVE-2025-46727](https://github.com/rack/rack/security/advisories/GHSA-gjh7-p2fx-99vx) Unbounded parameter parsing in `Rack::QueryParser` can lead to memory exhaustion.
561
+ - [CVE-2025-46727](https://github.com/advisories/GHSA-gjh7-p2fx-99vx) Unbounded parameter parsing in `Rack::QueryParser` can lead to memory exhaustion.
452
562
 
453
563
  ## [2.2.13] - 2025-03-11
454
564
 
455
565
  ### Security
456
566
 
457
- - [CVE-2025-27610](https://github.com/rack/rack/security/advisories/GHSA-7wqh-767x-r66v) Local file inclusion in `Rack::Static`.
567
+ - [CVE-2025-27610](https://github.com/advisories/GHSA-7wqh-767x-r66v) Local file inclusion in `Rack::Static`.
458
568
 
459
569
  ## [2.2.12] - 2025-03-04
460
570
 
461
571
  ### Security
462
572
 
463
- - [CVE-2025-27111](https://github.com/rack/rack/security/advisories/GHSA-8cgq-6mh2-7j6v) Possible Log Injection in `Rack::Sendfile`.
573
+ - [CVE-2025-27111](https://github.com/advisories/GHSA-8cgq-6mh2-7j6v) Possible Log Injection in `Rack::Sendfile`.
464
574
 
465
575
  ## [2.2.11] - 2025-02-12
466
576
 
467
577
  ### Security
468
578
 
469
- - [CVE-2025-25184](https://github.com/rack/rack/security/advisories/GHSA-7g2v-jj9q-g3rg) Possible Log Injection in `Rack::CommonLogger`.
579
+ - [CVE-2025-25184](https://github.com/advisories/GHSA-7g2v-jj9q-g3rg) Possible Log Injection in `Rack::CommonLogger`.
470
580
 
471
581
  ## [2.2.10] - 2024-10-14
472
582
 
data/README.md CHANGED
@@ -230,6 +230,14 @@ query string, before attempting parsing, so if the same parameter key is
230
230
  used multiple times in the query, each counts as a separate parameter for
231
231
  this check.
232
232
 
233
+ ### `RACK_MULTIPART_BUFFERED_UPLOAD_BYTESIZE_LIMIT`
234
+
235
+ This environment variable sets the maximum amount of memory Rack will use
236
+ to buffer multipart parameters when parsing a request body. This considers
237
+ the size of the multipart mime headers and the body part for multipart
238
+ parameters that are buffered in memory and do not use tempfiles. This
239
+ defaults to 16MB if not provided.
240
+
233
241
  ### `param_depth_limit`
234
242
 
235
243
  ```ruby
@@ -17,7 +17,7 @@ module Rack
17
17
  # If +app+ is not specified, a Rack::Files of the same +root+ will be used.
18
18
 
19
19
  class Directory
20
- 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"
20
+ 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"
21
21
  DIR_PAGE_HEADER = <<-PAGE
22
22
  <html><head>
23
23
  <title>%s</title>
@@ -51,7 +51,7 @@ table { width:100%%; }
51
51
  class DirectoryBody < Struct.new(:root, :path, :files)
52
52
  # Yield strings for each part of the directory entry
53
53
  def each
54
- show_path = Utils.escape_html(path.sub(/^#{root}/, ''))
54
+ show_path = Utils.escape_html(path.sub(/\A#{Regexp.escape(root)}/, ''))
55
55
  yield(DIR_PAGE_HEADER % [ show_path, show_path ])
56
56
 
57
57
  unless path.chomp('/') == root
@@ -82,6 +82,7 @@ table { width:100%%; }
82
82
  # Set the root directory and application for serving files.
83
83
  def initialize(root, app = nil)
84
84
  @root = ::File.expand_path(root)
85
+ @root_with_separator = @root.end_with?(::File::SEPARATOR) ? @root : "#{@root}#{::File::SEPARATOR}"
85
86
  @app = app || Files.new(@root)
86
87
  @head = Head.new(method(:get))
87
88
  end
@@ -118,7 +119,9 @@ table { width:100%%; }
118
119
  # Rack response to use for requests with paths outside the root, or nil if path is inside the root.
119
120
  def check_forbidden(path_info)
120
121
  return unless path_info.include? ".."
121
- return if ::File.expand_path(::File.join(@root, path_info)).start_with?(@root)
122
+
123
+ expanded_path = ::File.expand_path(::File.join(@root, path_info))
124
+ return if expanded_path == @root || expanded_path.start_with?(@root_with_separator)
122
125
 
123
126
  body = "Forbidden\n"
124
127
  [403, { CONTENT_TYPE => "text/plain",
data/lib/rack/files.rb CHANGED
@@ -194,7 +194,7 @@ EOF
194
194
  status,
195
195
  {
196
196
  CONTENT_TYPE => "text/plain",
197
- CONTENT_LENGTH => body.size.to_s,
197
+ CONTENT_LENGTH => body.bytesize.to_s,
198
198
  "x-cascade" => "pass"
199
199
  }.merge!(headers),
200
200
  [body]
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'stringio'
3
4
  require 'time'
4
5
 
5
6
  require_relative 'response'
@@ -82,8 +83,16 @@ module Rack
82
83
  # end
83
84
  buffer = @buffered_body = String.new
84
85
 
85
- @body.each do |chunk|
86
- 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)
87
96
  end
88
97
 
89
98
  return buffer
@@ -33,7 +33,7 @@ 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=\"?([^\";,]+)\"?|ni
36
+ MULTIPART = %r|\Amultipart/.*?boundary(\s*)=\"?([^\";,]+)\"?|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
@@ -59,6 +59,34 @@ module Rack
59
59
  Tempfile.new(["RackMultipart", extension])
60
60
  }
61
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
+
83
+ bytesize_limit = env_int.call("RACK_MULTIPART_PARSER_BYTESIZE_LIMIT", 10 * 1024 * 1024 * 1024)
84
+ PARSER_BYTESIZE_LIMIT = bytesize_limit > 0 ? bytesize_limit : nil
85
+ private_constant :PARSER_BYTESIZE_LIMIT
86
+
87
+ CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT = env_int.call("RACK_MULTIPART_CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT", 8 * 1024)
88
+ private_constant :CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT
89
+
62
90
  class BoundedIO # :nodoc:
63
91
  def initialize(io, content_length)
64
92
  @io = io
@@ -95,7 +123,15 @@ module Rack
95
123
  return unless content_type
96
124
  data = content_type.match(MULTIPART)
97
125
  return unless data
98
- data[1]
126
+
127
+ unless data[1].empty?
128
+ raise Error, "whitespace between boundary parameter name and equal sign"
129
+ end
130
+ if data.post_match.match?(/boundary\s*=/i)
131
+ raise BoundaryTooLongError, "multiple boundary parameters found in multipart content type"
132
+ end
133
+
134
+ data[2]
99
135
  end
100
136
 
101
137
  def self.parse(io, content_length, content_type, tmpfile, bufsize, qp)
@@ -104,6 +140,10 @@ module Rack
104
140
  boundary = parse_boundary content_type
105
141
  return EMPTY unless boundary
106
142
 
143
+ if PARSER_BYTESIZE_LIMIT && content_length && content_length > PARSER_BYTESIZE_LIMIT
144
+ raise Error, "multipart Content-Length #{content_length} exceeds limit of #{PARSER_BYTESIZE_LIMIT} bytes"
145
+ end
146
+
107
147
  if boundary.length > 70
108
148
  # RFC 1521 Section 7.2.1 imposes a 70 character maximum for the boundary.
109
149
  # Most clients use no more than 55 characters.
@@ -218,6 +258,10 @@ module Rack
218
258
 
219
259
  @state = :FAST_FORWARD
220
260
  @mime_index = 0
261
+ @body_retained = nil
262
+ @retained_size = 0
263
+ @total_bytes_read = (0 if PARSER_BYTESIZE_LIMIT)
264
+ @content_disposition_quoted_escapes = 0
221
265
  @collector = Collector.new tempfile
222
266
 
223
267
  @sbuf = StringScanner.new("".dup)
@@ -229,6 +273,7 @@ module Rack
229
273
  end
230
274
 
231
275
  def parse(io)
276
+ @total_bytes_read &&= nil if io.is_a?(BoundedIO)
232
277
  outbuf = String.new
233
278
  read_data(io, outbuf)
234
279
 
@@ -267,6 +312,12 @@ module Rack
267
312
  def read_data(io, outbuf)
268
313
  content = io.read(@bufsize, outbuf)
269
314
  handle_empty_content!(content)
315
+ if @total_bytes_read
316
+ @total_bytes_read += content.bytesize
317
+ if @total_bytes_read > PARSER_BYTESIZE_LIMIT
318
+ raise Error, "multipart upload exceeds limit of #{PARSER_BYTESIZE_LIMIT} bytes"
319
+ end
320
+ end
270
321
  @sbuf.concat(content)
271
322
  end
272
323
 
@@ -294,6 +345,10 @@ module Rack
294
345
 
295
346
  # retry for opening boundary
296
347
  else
348
+ # We raise if we don't find the multipart boundary, to avoid unbounded memory
349
+ # buffering. Note that the actual limit is the higher of 16KB and the buffer size (1MB by default)
350
+ raise Error, "multipart boundary not found within limit" if @sbuf.string.bytesize > BOUNDARY_START_LIMIT
351
+
297
352
  # no boundary found, keep reading data
298
353
  return :want_read
299
354
  end
@@ -312,13 +367,21 @@ module Rack
312
367
 
313
368
  CONTENT_DISPOSITION_MAX_PARAMS = 16
314
369
  CONTENT_DISPOSITION_MAX_BYTES = 1536
370
+ OBS_UNFOLD = /\r\n([ \t])/
371
+ private_constant :OBS_UNFOLD
372
+
315
373
  def handle_mime_head
316
374
  if @sbuf.scan_until(@head_regex)
317
375
  head = @sbuf[1]
318
376
  content_type = head[MULTIPART_CONTENT_TYPE, 1]
377
+ content_type.gsub!(OBS_UNFOLD, '\1') if content_type
378
+
319
379
  if (disposition = head[MULTIPART_CONTENT_DISPOSITION, 1]) &&
320
380
  disposition.bytesize <= CONTENT_DISPOSITION_MAX_BYTES
321
381
 
382
+ # Implement OBS unfolding (RFC 5322 Section 2.2.3)
383
+ disposition.gsub!(OBS_UNFOLD, '\1')
384
+
322
385
  # ignore actual content-disposition value (should always be form-data)
323
386
  i = disposition.index(';')
324
387
  disposition.slice!(0, i+1)
@@ -356,6 +419,11 @@ module Rack
356
419
  # stop parsing parameter value if found ending quote
357
420
  break if c == '"'
358
421
 
422
+ @content_disposition_quoted_escapes += 1
423
+ if @content_disposition_quoted_escapes > CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT
424
+ raise Error, "number of quoted escapes during content disposition parsing exceeds limit"
425
+ end
426
+
359
427
  escaped_char = disposition.slice!(0, 1)
360
428
  if param == 'filename' && escaped_char != '"'
361
429
  # Possible IE uploaded filename, append both escape backslash and value
@@ -410,16 +478,30 @@ module Rack
410
478
  name = filename || "#{content_type || TEXT_PLAIN}[]".dup
411
479
  end
412
480
 
481
+ # Mime part head data is retained for both TempfilePart and BufferPart
482
+ # for the entireity of the parse, even though it isn't used for BufferPart.
483
+ update_retained_size(head.bytesize)
484
+
485
+ # If a filename is given, a TempfilePart will be used, so the body will
486
+ # not be buffered in memory. However, if a filename is not given, a BufferPart
487
+ # will be used, and the body will be buffered in memory.
488
+ @body_retained = !filename
489
+
413
490
  @collector.on_mime_head @mime_index, head, filename, content_type, name
414
491
  @state = :MIME_BODY
415
492
  else
416
- :want_read
493
+ # We raise if the mime part header is too large, to avoid unbounded memory
494
+ # buffering. Note that the actual limit is the higher of 64KB and the buffer size (1MB by default)
495
+ raise Error, "multipart mime part header too large" if @sbuf.rest.bytesize > MIME_HEADER_BYTESIZE_LIMIT
496
+
497
+ return :want_read
417
498
  end
418
499
  end
419
500
 
420
501
  def handle_mime_body
421
502
  if (body_with_boundary = @sbuf.check_until(@body_regex)) # check but do not advance the pointer yet
422
503
  body = body_with_boundary.sub(@body_regex_at_end, '') # remove the boundary from the string
504
+ update_retained_size(body.bytesize) if @body_retained
423
505
  @collector.on_mime_body @mime_index, body
424
506
  @sbuf.pos += body.length + 2 # skip \r\n after the content
425
507
  @state = :CONSUME_TOKEN
@@ -428,7 +510,9 @@ module Rack
428
510
  # Save what we have so far
429
511
  if @rx_max_size < @sbuf.rest_size
430
512
  delta = @sbuf.rest_size - @rx_max_size
431
- @collector.on_mime_body @mime_index, @sbuf.peek(delta)
513
+ body = @sbuf.peek(delta)
514
+ update_retained_size(body.bytesize) if @body_retained
515
+ @collector.on_mime_body @mime_index, body
432
516
  @sbuf.pos += delta
433
517
  @sbuf.string = @sbuf.rest
434
518
  end
@@ -436,6 +520,13 @@ module Rack
436
520
  end
437
521
  end
438
522
 
523
+ def update_retained_size(size)
524
+ @retained_size += size
525
+ if @retained_size > BUFFERED_UPLOAD_BYTESIZE_LIMIT
526
+ raise Error, "multipart data over retained size limit"
527
+ end
528
+ end
529
+
439
530
  # Scan until the we find the start or end of the boundary.
440
531
  # If we find it, return the appropriate symbol for the start or
441
532
  # end of the boundary. If we don't find the start or end of the
@@ -57,6 +57,8 @@ 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
+
60
62
  def initialize(params_class, param_depth_limit, bytesize_limit: BYTESIZE_LIMIT, params_limit: PARAMS_LIMIT)
61
63
  @params_class = params_class
62
64
  @param_depth_limit = param_depth_limit
@@ -221,7 +223,7 @@ module Rack
221
223
  return if !qs || qs.empty?
222
224
 
223
225
  if qs.bytesize > @bytesize_limit
224
- raise QueryLimitError, "total query size (#{qs.bytesize}) exceeds limit (#{@bytesize_limit})"
226
+ raise QueryLimitError, "total query size exceeds limit (#{@bytesize_limit})"
225
227
  end
226
228
 
227
229
  pairs = qs.split(separator ? (COMMON_SEP[separator] || /[#{separator}] */n) : DEFAULT_SEP, @params_limit + 1)
data/lib/rack/request.rb CHANGED
@@ -513,7 +513,10 @@ module Rack
513
513
  if pairs = Rack::Multipart.parse_multipart(env, Rack::Multipart::ParamList)
514
514
  set_header RACK_REQUEST_FORM_PAIRS, pairs
515
515
  else
516
- form_vars = get_header(RACK_INPUT).read
516
+ # Add 2 bytes. One to check whether it is over the limit, and a second
517
+ # in case the slice! call below removes the last byte
518
+ # If read returns nil, use the empty string
519
+ form_vars = get_header(RACK_INPUT).read(query_parser.bytesize_limit + 2) || ''
517
520
 
518
521
  # Fix for Safari Ajax postings that always append \0
519
522
  # form_vars.sub!(/\0\z/, '') # performance replacement:
@@ -720,8 +723,8 @@ module Rack
720
723
  # Match IPv6 as a string of hex digits and colons in square brackets
721
724
  \[(?<address>#{ipv6})\]
722
725
  |
723
- # Match any other printable string (except square brackets) as a hostname
724
- (?<address>[[[:graph:]&&[^\[\]]]]*?)
726
+ # Match characters allowed by RFC 3986 Section 3.2.2
727
+ (?<address>[-a-zA-Z0-9._~%!$&'()*+,;=]*?)
725
728
  )
726
729
  (:(?<port>\d+))?
727
730
  \z
data/lib/rack/sendfile.rb CHANGED
@@ -16,21 +16,21 @@ module Rack
16
16
  # delivery code.
17
17
  #
18
18
  # In order to take advantage of this middleware, the response body must
19
- # respond to +to_path+ and the request must include an x-sendfile-type
19
+ # respond to +to_path+ and the request must include an `x-sendfile-type`
20
20
  # header. Rack::Files and other components implement +to_path+ so there's
21
- # rarely anything you need to do in your application. The x-sendfile-type
21
+ # rarely anything you need to do in your application. The `x-sendfile-type`
22
22
  # header is typically set in your web servers configuration. The following
23
23
  # sections attempt to document
24
24
  #
25
25
  # === Nginx
26
26
  #
27
- # Nginx supports the x-accel-redirect header. This is similar to x-sendfile
27
+ # Nginx supports the `x-accel-redirect` header. This is similar to `x-sendfile`
28
28
  # but requires parts of the filesystem to be mapped into a private URL
29
29
  # hierarchy.
30
30
  #
31
31
  # The following example shows the Nginx configuration required to create
32
- # a private "/files/" area, enable x-accel-redirect, and pass the special
33
- # x-sendfile-type and x-accel-mapping headers to the backend:
32
+ # a private "/files/" area, enable `x-accel-redirect`, and pass the special
33
+ # `x-accel-mapping` header to the backend:
34
34
  #
35
35
  # location ~ /files/(.*) {
36
36
  # internal;
@@ -44,24 +44,29 @@ module Rack
44
44
  # proxy_set_header X-Real-IP $remote_addr;
45
45
  # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
46
46
  #
47
- # proxy_set_header x-sendfile-type x-accel-redirect;
48
47
  # proxy_set_header x-accel-mapping /var/www/=/files/;
49
48
  #
50
49
  # proxy_pass http://127.0.0.1:8080/;
51
50
  # }
52
51
  #
53
- # Note that the x-sendfile-type header must be set exactly as shown above.
54
- # The x-accel-mapping header should specify the location on the file system,
52
+ # The `x-accel-mapping` header should specify the location on the file system,
55
53
  # followed by an equals sign (=), followed name of the private URL pattern
56
- # that it maps to. The middleware performs a simple substitution on the
54
+ # that it maps to. The middleware performs a case-insensitive substitution on the
57
55
  # resulting path.
58
56
  #
57
+ # To enable `x-accel-redirect`, you must configure the middleware explicitly:
58
+ #
59
+ # use Rack::Sendfile, "x-accel-redirect"
60
+ #
61
+ # For security reasons, the `x-sendfile-type` header from requests is ignored.
62
+ # The sendfile variation must be set via the middleware constructor.
63
+ #
59
64
  # See Also: https://www.nginx.com/resources/wiki/start/topics/examples/xsendfile
60
65
  #
61
66
  # === lighttpd
62
67
  #
63
- # Lighttpd has supported some variation of the x-sendfile header for some
64
- # time, although only recent version support x-sendfile in a reverse proxy
68
+ # Lighttpd has supported some variation of the `x-sendfile` header for some
69
+ # time, although only recent version support `x-sendfile` in a reverse proxy
65
70
  # configuration.
66
71
  #
67
72
  # $HTTP["host"] == "example.com" {
@@ -83,7 +88,7 @@ module Rack
83
88
  #
84
89
  # === Apache
85
90
  #
86
- # x-sendfile is supported under Apache 2.x using a separate module:
91
+ # `x-sendfile` is supported under Apache 2.x using a separate module:
87
92
  #
88
93
  # https://tn123.org/mod_xsendfile/
89
94
  #
@@ -97,16 +102,28 @@ module Rack
97
102
  # === Mapping parameter
98
103
  #
99
104
  # The third parameter allows for an overriding extension of the
100
- # x-accel-mapping header. Mappings should be provided in tuples of internal to
105
+ # `x-accel-mapping` header. Mappings should be provided in tuples of internal to
101
106
  # external. The internal values may contain regular expression syntax, they
102
107
  # will be matched with case indifference.
108
+ #
109
+ # When `x-accel-redirect` is explicitly enabled via the variation parameter,
110
+ # and no application-level mappings are provided, the middleware will read
111
+ # the `x-accel-mapping` header from the proxy. This allows nginx to control
112
+ # the path mapping without requiring application-level configuration.
113
+ #
114
+ # === Security
115
+ #
116
+ # For security reasons, the `x-sendfile-type` header from HTTP requests is
117
+ # ignored. The sendfile variation must be explicitly configured via the
118
+ # middleware constructor to prevent information disclosure vulnerabilities
119
+ # where attackers could bypass proxy restrictions.
103
120
 
104
121
  class Sendfile
105
122
  def initialize(app, variation = nil, mappings = [])
106
123
  @app = app
107
124
  @variation = variation
108
125
  @mappings = mappings.map do |internal, external|
109
- [/^#{internal}/i, external]
126
+ [/\A#{internal}/i, external]
110
127
  end
111
128
  end
112
129
 
@@ -145,22 +162,35 @@ module Rack
145
162
  end
146
163
 
147
164
  private
165
+
148
166
  def variation(env)
149
- @variation ||
150
- env['sendfile.type'] ||
151
- env['HTTP_X_SENDFILE_TYPE']
167
+ # Note: HTTP_X_SENDFILE_TYPE is intentionally NOT read for security reasons.
168
+ # Attackers could use this header to enable x-accel-redirect and bypass proxy restrictions.
169
+ @variation || env['sendfile.type']
170
+ end
171
+
172
+ def x_accel_mapping(env)
173
+ # Only allow header when:
174
+ # 1. `x-accel-redirect` is explicitly enabled via constructor.
175
+ # 2. No application-level mappings are configured.
176
+ return nil unless @variation =~ /x-accel-redirect/i
177
+ return nil if @mappings.any?
178
+
179
+ env['HTTP_X_ACCEL_MAPPING']
152
180
  end
153
181
 
154
182
  def map_accel_path(env, path)
155
183
  if mapping = @mappings.find { |internal, _| internal =~ path }
156
- path.sub(*mapping)
157
- elsif mapping = env['HTTP_X_ACCEL_MAPPING']
184
+ return path.sub(*mapping)
185
+ elsif mapping = x_accel_mapping(env)
186
+ # Safe to use header: explicit config + no app mappings:
158
187
  mapping.split(',').map(&:strip).each do |m|
159
188
  internal, external = m.split('=', 2).map(&:strip)
160
- new_path = path.sub(/^#{internal}/i, external)
189
+ new_path = path.sub(/\A#{Regexp.escape(internal)}/i, external)
161
190
  return new_path unless path == new_path
162
191
  end
163
- path
192
+
193
+ return path
164
194
  end
165
195
  end
166
196
  end
data/lib/rack/static.rb CHANGED
@@ -93,6 +93,9 @@ module Rack
93
93
  def initialize(app, options = {})
94
94
  @app = app
95
95
  @urls = options[:urls] || ["/favicon.ico"]
96
+ if @urls.kind_of?(Array)
97
+ @urls = @urls.map { |url| [url, url.end_with?('/') ? url : "#{url}/".freeze].freeze }.freeze
98
+ end
96
99
  @index = options[:index]
97
100
  @gzip = options[:gzip]
98
101
  @cascade = options[:cascade]
@@ -115,7 +118,7 @@ module Rack
115
118
  end
116
119
 
117
120
  def route_file(path)
118
- @urls.kind_of?(Array) && @urls.any? { |url| path.index(url) == 0 }
121
+ @urls.kind_of?(Array) && @urls.any? { |url, url_slash| path == url || path.start_with?(url_slash) }
119
122
  end
120
123
 
121
124
  def can_serve(path)
@@ -165,6 +168,8 @@ module Rack
165
168
 
166
169
  # Convert HTTP header rules to HTTP headers
167
170
  def applicable_rules(path)
171
+ path = ::Rack::Utils.unescape_path(path)
172
+
168
173
  @header_rules.find_all do |rule, new_headers|
169
174
  case rule
170
175
  when :all
@@ -172,10 +177,9 @@ module Rack
172
177
  when :fonts
173
178
  /\.(?:ttf|otf|eot|woff2|woff|svg)\z/.match?(path)
174
179
  when String
175
- path = ::Rack::Utils.unescape(path)
176
180
  path.start_with?(rule) || path.start_with?('/' + rule)
177
181
  when Array
178
- /\.(#{rule.join('|')})\z/.match?(path)
182
+ /\.#{Regexp.union(rule)}\z/.match?(path)
179
183
  when Regexp
180
184
  rule.match?(path)
181
185
  else
data/lib/rack/utils.rb CHANGED
@@ -146,17 +146,77 @@ module Rack
146
146
  end
147
147
  end
148
148
 
149
+ ALLOWED_FORWARED_PARAMS = %w[by for host proto].to_h { |name| [name, name.to_sym] }.freeze
150
+ private_constant :ALLOWED_FORWARED_PARAMS
151
+
149
152
  def forwarded_values(forwarded_header)
150
- return nil unless forwarded_header
151
- forwarded_header = forwarded_header.to_s.gsub("\n", ";")
152
-
153
- forwarded_header.split(';').each_with_object({}) do |field, values|
154
- field.split(',').each do |pair|
155
- pair = pair.split('=').map(&:strip).join('=')
156
- return nil unless pair =~ /\A(by|for|host|proto)="?([^"]+)"?\Z/i
157
- (values[$1.downcase.to_sym] ||= []) << $2
153
+ return unless forwarded_header
154
+ header = forwarded_header.to_s.tr("\n", ";")
155
+ header.sub!(/\A[\s;,]+/, '')
156
+ num_params = num_escapes = 0
157
+ max_params = max_escapes = 1024
158
+ params = {}
159
+
160
+ # Parse parameter list
161
+ while i = header.index('=')
162
+ # Only parse up to max parameters, to avoid potential denial of service
163
+ num_params += 1
164
+ return if num_params > max_params
165
+
166
+ # Found end of parameter name, ensure forward progress in loop
167
+ param = header.slice!(0, i+1)
168
+
169
+ # Remove ending equals and preceding whitespace from parameter name
170
+ param.chomp!('=')
171
+ param.strip!
172
+ param.downcase!
173
+ return unless param = ALLOWED_FORWARED_PARAMS[param]
174
+
175
+ if header[0] == '"'
176
+ # Parameter value is quoted, parse it, handling backslash escapes
177
+ header.slice!(0, 1)
178
+ value = String.new
179
+
180
+ while i = header.index(/(["\\])/)
181
+ c = $1
182
+
183
+ # Append all content until ending quote or escape
184
+ value << header.slice!(0, i)
185
+
186
+ # Remove either backslash or ending quote,
187
+ # ensures forward progress in loop
188
+ header.slice!(0, 1)
189
+
190
+ # stop parsing parameter value if found ending quote
191
+ break if c == '"'
192
+
193
+ # Only allow up to max escapes, to avoid potential denial of service
194
+ num_escapes += 1
195
+ return if num_escapes > max_escapes
196
+ escaped_char = header.slice!(0, 1)
197
+ value << escaped_char
198
+ end
199
+ else
200
+ if i = header.index(/[;,]/)
201
+ # Parameter value unquoted (which may be invalid), value ends at comma or semicolon
202
+ value = header.slice!(0, i)
203
+ value.sub!(/[\s;,]+\z/, '')
204
+ else
205
+ # If no ending semicolon, assume remainder of line is value and stop parsing
206
+ header.strip!
207
+ value = header
208
+ header = ''
209
+ end
210
+ value.lstrip!
158
211
  end
212
+
213
+ (params[param] ||= []) << value
214
+
215
+ # skip trailing semicolons/commas/whitespace, to proceed to next parameter
216
+ header.sub!(/\A[\s;,]+/, '') unless header.empty?
159
217
  end
218
+
219
+ params
160
220
  end
161
221
  module_function :forwarded_values
162
222
 
@@ -193,17 +253,41 @@ module Rack
193
253
  # :nocov:
194
254
  end
195
255
 
256
+ # Given an array of available encoding strings, and an array of
257
+ # acceptable encodings for a request, where each element of the
258
+ # acceptable encodings array is an array where the first element
259
+ # is an encoding name and the second element is the numeric
260
+ # priority for the encoding, return the available encoding with
261
+ # the highest priority.
262
+ #
263
+ # The accept_encoding argument is typically generated by calling
264
+ # Request#accept_encoding.
265
+ #
266
+ # Example:
267
+ #
268
+ # select_best_encoding(%w(compress gzip identity),
269
+ # [["compress", 0.5], ["gzip", 1.0]])
270
+ # # => "gzip"
271
+ #
272
+ # To reduce denial of service potential, only the first 16
273
+ # acceptable encodings are considered.
196
274
  def select_best_encoding(available_encodings, accept_encoding)
197
275
  # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
198
276
 
277
+ # Only process the first 16 encodings
278
+ accept_encoding = accept_encoding[0...16]
199
279
  expanded_accept_encoding = []
280
+ wildcard_seen = false
200
281
 
201
282
  accept_encoding.each do |m, q|
202
283
  preference = available_encodings.index(m) || available_encodings.size
203
284
 
204
285
  if m == "*"
205
- (available_encodings - accept_encoding.map(&:first)).each do |m2|
206
- expanded_accept_encoding << [m2, q, preference]
286
+ unless wildcard_seen
287
+ (available_encodings - accept_encoding.map(&:first)).each do |m2|
288
+ expanded_accept_encoding << [m2, q, preference]
289
+ end
290
+ wildcard_seen = true
207
291
  end
208
292
  else
209
293
  expanded_accept_encoding << [m, q, preference]
@@ -211,7 +295,13 @@ module Rack
211
295
  end
212
296
 
213
297
  encoding_candidates = expanded_accept_encoding
214
- .sort_by { |_, q, p| [-q, p] }
298
+ .sort do |(_, q1, p1), (_, q2, p2)|
299
+ if r = (q1 <=> q2).nonzero?
300
+ -r
301
+ else
302
+ (p1 <=> p2).nonzero? || 0
303
+ end
304
+ end
215
305
  .map!(&:first)
216
306
 
217
307
  unless encoding_candidates.include?("identity")
@@ -399,17 +489,19 @@ module Rack
399
489
  # Parses the "Range:" header, if present, into an array of Range objects.
400
490
  # Returns nil if the header is missing or syntactically invalid.
401
491
  # Returns an empty array if none of the ranges are satisfiable.
402
- def byte_ranges(env, size)
403
- get_byte_ranges env['HTTP_RANGE'], size
492
+ def byte_ranges(env, size, max_ranges: 100)
493
+ get_byte_ranges env['HTTP_RANGE'], size, max_ranges: max_ranges
404
494
  end
405
495
 
406
- def get_byte_ranges(http_range, size)
496
+ def get_byte_ranges(http_range, size, max_ranges: 100)
407
497
  # See <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35>
408
498
  # Ignore Range when file size is 0 to avoid a 416 error.
409
499
  return nil if size.zero?
410
500
  return nil unless http_range && http_range =~ /bytes=([^;]+)/
501
+ byte_range = $1
502
+ return nil if byte_range.count(',') >= max_ranges
411
503
  ranges = []
412
- $1.split(/,[ \t]*/).each do |range_spec|
504
+ byte_range.split(/,[ \t]*/).each do |range_spec|
413
505
  return nil unless range_spec.include?('-')
414
506
  range = range_spec.split('-')
415
507
  r0, r1 = range[0], range[1]
data/lib/rack/version.rb CHANGED
@@ -6,7 +6,7 @@
6
6
  # See MIT-LICENSE or https://opensource.org/licenses/MIT.
7
7
 
8
8
  module Rack
9
- VERSION = "3.2.1"
9
+ VERSION = "3.2.6"
10
10
 
11
11
  RELEASE = VERSION
12
12
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rack
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.2.1
4
+ version: 3.2.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Leah Neukirchen
@@ -156,7 +156,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
156
156
  - !ruby/object:Gem::Version
157
157
  version: '0'
158
158
  requirements: []
159
- rubygems_version: 3.6.9
159
+ rubygems_version: 4.0.6
160
160
  specification_version: 4
161
161
  summary: A modular Ruby webserver interface.
162
162
  test_files: []