puma 5.6.0-java → 5.6.4-java

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

Potentially problematic release.


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

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1c9604a002b072d351254eef56c4cc52bc7ac1c73750546f0408a7bd21f91705
4
- data.tar.gz: 24919b065100d37b1f43b7465a9464e6b5492a0256d7880ea6f519a482deafe5
3
+ metadata.gz: 00fb5299edf47cd69e241fc63a66cb98ef7180e0df430651bab94adeec19bc9f
4
+ data.tar.gz: 70089c146221037a050001dbeca34da0be535268994214eace5d9f1c670684b6
5
5
  SHA512:
6
- metadata.gz: ab900da240f92390b6f4235bef4f0358cc03d13cb795b44c878d9b96d0ea2865d35419cbe8c31f2fc18cb272b9df716273959391b85a6455aaebdadd03b2922b
7
- data.tar.gz: 2daeb82d068ebd639a27699f670df828417069eba86fb3c4c599371898079cafcae7c0d5ac7256b562b8b0629f4e236ff9e62a17efc80ba0c00fca8ca3cfcae8
6
+ metadata.gz: da00b1d9d4d509cbe4f5da700091188b6559a61c932331cf7e4aef8f424b426846296df0804cb5b06f23033a3d5b6d8d31dcb512444ae276b0071b3e653f5f2e
7
+ data.tar.gz: efe8517f061dc996c83cec30110c3b8a84e6563dcba33e6416ad98799f685e0145bbd6856c7b7941cbf5682de44c73b56221022ca8231b8cbee7afe5faa78c15
data/History.md CHANGED
@@ -1,3 +1,18 @@
1
+ ## 5.6.4 / 2022-03-30
2
+
3
+ * Security
4
+ * Close several HTTP Request Smuggling exploits (CVE-2022-24790)
5
+
6
+ ## 5.6.2 / 2022-02-11
7
+
8
+ * Bugfix/Security
9
+ * Response body will always be `close`d. (GHSA-rmj8-8hhh-gv5h, related to [#2809])
10
+
11
+ ## 5.6.1 / 2022-01-26
12
+
13
+ * Bugfixes
14
+ * Reverted a commit which appeared to be causing occasional blank header values ([#2809])
15
+
1
16
  ## 5.6.0 / 2022-01-25
2
17
 
3
18
  * Features
@@ -1830,6 +1845,7 @@ be added back in a future date when a java Puma::MiniSSL is added.
1830
1845
  * Bugfixes
1831
1846
  * Your bugfix goes here <Most recent on the top, like GitHub> (#Github Number)
1832
1847
 
1848
+ [#2809]:https://github.com/puma/puma/pull/2809 "PR by @dentarg, merged 2022-01-26"
1833
1849
  [#2764]:https://github.com/puma/puma/pull/2764 "PR by @dentarg, merged 2022-01-18"
1834
1850
  [#2708]:https://github.com/puma/puma/issues/2708 "Issue by @erikaxel, closed 2022-01-18"
1835
1851
  [#2780]:https://github.com/puma/puma/pull/2780 "PR by @dalibor, merged 2022-01-01"
data/lib/puma/client.rb CHANGED
@@ -23,6 +23,8 @@ module Puma
23
23
 
24
24
  class ConnectionError < RuntimeError; end
25
25
 
26
+ class HttpParserError501 < IOError; end
27
+
26
28
  # An instance of this class represents a unique request from a client.
27
29
  # For example, this could be a web request from a browser or from CURL.
28
30
  #
@@ -35,7 +37,21 @@ module Puma
35
37
  # Instances of this class are responsible for knowing if
36
38
  # the header and body are fully buffered via the `try_to_finish` method.
37
39
  # They can be used to "time out" a response via the `timeout_at` reader.
40
+ #
38
41
  class Client
42
+
43
+ # this tests all values but the last, which must be chunked
44
+ ALLOWED_TRANSFER_ENCODING = %w[compress deflate gzip].freeze
45
+
46
+ # chunked body validation
47
+ CHUNK_SIZE_INVALID = /[^\h]/.freeze
48
+ CHUNK_VALID_ENDING = "\r\n".freeze
49
+
50
+ # Content-Length header value validation
51
+ CONTENT_LENGTH_VALUE_INVALID = /[^\d]/.freeze
52
+
53
+ TE_ERR_MSG = 'Invalid Transfer-Encoding'
54
+
39
55
  # The object used for a request with no body. All requests with
40
56
  # no body share this one object since it has no state.
41
57
  EmptyBody = NullIO.new
@@ -302,16 +318,27 @@ module Puma
302
318
  body = @parser.body
303
319
 
304
320
  te = @env[TRANSFER_ENCODING2]
305
-
306
321
  if te
307
- if te.include?(",")
308
- te.split(",").each do |part|
309
- if CHUNKED.casecmp(part.strip) == 0
310
- return setup_chunked_body(body)
311
- end
322
+ te_lwr = te.downcase
323
+ if te.include? ','
324
+ te_ary = te_lwr.split ','
325
+ te_count = te_ary.count CHUNKED
326
+ te_valid = te_ary[0..-2].all? { |e| ALLOWED_TRANSFER_ENCODING.include? e }
327
+ if te_ary.last == CHUNKED && te_count == 1 && te_valid
328
+ @env.delete TRANSFER_ENCODING2
329
+ return setup_chunked_body body
330
+ elsif te_count >= 1
331
+ raise HttpParserError , "#{TE_ERR_MSG}, multiple chunked: '#{te}'"
332
+ elsif !te_valid
333
+ raise HttpParserError501, "#{TE_ERR_MSG}, unknown value: '#{te}'"
312
334
  end
313
- elsif CHUNKED.casecmp(te) == 0
314
- return setup_chunked_body(body)
335
+ elsif te_lwr == CHUNKED
336
+ @env.delete TRANSFER_ENCODING2
337
+ return setup_chunked_body body
338
+ elsif ALLOWED_TRANSFER_ENCODING.include? te_lwr
339
+ raise HttpParserError , "#{TE_ERR_MSG}, single value must be chunked: '#{te}'"
340
+ else
341
+ raise HttpParserError501 , "#{TE_ERR_MSG}, unknown value: '#{te}'"
315
342
  end
316
343
  end
317
344
 
@@ -319,7 +346,12 @@ module Puma
319
346
 
320
347
  cl = @env[CONTENT_LENGTH]
321
348
 
322
- unless cl
349
+ if cl
350
+ # cannot contain characters that are not \d
351
+ if cl =~ CONTENT_LENGTH_VALUE_INVALID
352
+ raise HttpParserError, "Invalid Content-Length: #{cl.inspect}"
353
+ end
354
+ else
323
355
  @buffer = body.empty? ? nil : body
324
356
  @body = EmptyBody
325
357
  set_ready
@@ -478,7 +510,13 @@ module Puma
478
510
  while !io.eof?
479
511
  line = io.gets
480
512
  if line.end_with?("\r\n")
481
- len = line.strip.to_i(16)
513
+ # Puma doesn't process chunk extensions, but should parse if they're
514
+ # present, which is the reason for the semicolon regex
515
+ chunk_hex = line.strip[/\A[^;]+/]
516
+ if chunk_hex =~ CHUNK_SIZE_INVALID
517
+ raise HttpParserError, "Invalid chunk size: '#{chunk_hex}'"
518
+ end
519
+ len = chunk_hex.to_i(16)
482
520
  if len == 0
483
521
  @in_last_chunk = true
484
522
  @body.rewind
@@ -509,7 +547,12 @@ module Puma
509
547
 
510
548
  case
511
549
  when got == len
512
- write_chunk(part[0..-3]) # to skip the ending \r\n
550
+ # proper chunked segment must end with "\r\n"
551
+ if part.end_with? CHUNK_VALID_ENDING
552
+ write_chunk(part[0..-3]) # to skip the ending \r\n
553
+ else
554
+ raise HttpParserError, "Chunk size mismatch"
555
+ end
513
556
  when got <= len - 2
514
557
  write_chunk(part)
515
558
  @partial_part_left = len - part.size
data/lib/puma/const.rb CHANGED
@@ -76,7 +76,7 @@ module Puma
76
76
  508 => 'Loop Detected',
77
77
  510 => 'Not Extended',
78
78
  511 => 'Network Authentication Required'
79
- }
79
+ }.freeze
80
80
 
81
81
  # For some HTTP status codes the client only expects headers.
82
82
  #
@@ -85,7 +85,7 @@ module Puma
85
85
  204 => true,
86
86
  205 => true,
87
87
  304 => true
88
- }
88
+ }.freeze
89
89
 
90
90
  # Frequently used constants when constructing requests or responses. Many times
91
91
  # the constant just refers to a string with the same contents. Using these constants
@@ -100,7 +100,7 @@ module Puma
100
100
  # too taxing on performance.
101
101
  module Const
102
102
 
103
- PUMA_VERSION = VERSION = "5.6.0".freeze
103
+ PUMA_VERSION = VERSION = "5.6.4".freeze
104
104
  CODE_NAME = "Birdie's Version".freeze
105
105
 
106
106
  PUMA_SERVER_STRING = ['puma', PUMA_VERSION, CODE_NAME].join(' ').freeze
@@ -145,9 +145,11 @@ module Puma
145
145
  408 => "HTTP/1.1 408 Request Timeout\r\nConnection: close\r\nServer: Puma #{PUMA_VERSION}\r\n\r\n".freeze,
146
146
  # Indicate that there was an internal error, obviously.
147
147
  500 => "HTTP/1.1 500 Internal Server Error\r\n\r\n".freeze,
148
+ # Incorrect or invalid header value
149
+ 501 => "HTTP/1.1 501 Not Implemented\r\n\r\n".freeze,
148
150
  # A common header for indicating the server is too busy. Not used yet.
149
151
  503 => "HTTP/1.1 503 Service Unavailable\r\n\r\nBUSY".freeze
150
- }
152
+ }.freeze
151
153
 
152
154
  # The basic max request size we'll try to read.
153
155
  CHUNK_SIZE = 16 * 1024
Binary file
data/lib/puma/request.rb CHANGED
@@ -46,7 +46,11 @@ module Puma
46
46
  env[HIJACK_P] = true
47
47
  env[HIJACK] = client
48
48
 
49
- env[RACK_INPUT] = client.body
49
+ body = client.body
50
+
51
+ head = env[REQUEST_METHOD] == HEAD
52
+
53
+ env[RACK_INPUT] = body
50
54
  env[RACK_URL_SCHEME] ||= default_server_port(env) == PORT_443 ? HTTPS : HTTP
51
55
 
52
56
  if @early_hints
@@ -65,58 +69,36 @@ module Puma
65
69
  # A rack extension. If the app writes #call'ables to this
66
70
  # array, we will invoke them when the request is done.
67
71
  #
68
- env[RACK_AFTER_REPLY] = []
72
+ after_reply = env[RACK_AFTER_REPLY] = []
69
73
 
70
74
  begin
71
- status, headers, res_body = @thread_pool.with_force_shutdown do
72
- @app.call(env)
73
- end
74
-
75
- return :async if client.hijacked
76
-
77
- status = status.to_i
78
-
79
- if status == -1
80
- unless headers.empty? and res_body == []
81
- raise "async response must have empty headers and body"
75
+ begin
76
+ status, headers, res_body = @thread_pool.with_force_shutdown do
77
+ @app.call(env)
82
78
  end
83
79
 
84
- return :async
85
- end
86
- rescue ThreadPool::ForceShutdown => e
87
- @events.unknown_error e, client, "Rack app"
88
- @events.log "Detected force shutdown of a thread"
80
+ return :async if client.hijacked
89
81
 
90
- status, headers, res_body = lowlevel_error(e, env, 503)
91
- rescue Exception => e
92
- @events.unknown_error e, client, "Rack app"
93
-
94
- status, headers, res_body = lowlevel_error(e, env, 500)
95
- end
82
+ status = status.to_i
96
83
 
97
- write_response(status, headers, res_body, lines, requests, client)
98
- end
84
+ if status == -1
85
+ unless headers.empty? and res_body == []
86
+ raise "async response must have empty headers and body"
87
+ end
99
88
 
100
- # Does the actual response writing for Request#handle_request and Server#client_error
101
- #
102
- # @param status [Integer] the status returned by the Rack application
103
- # @param headers [Hash] the headers returned by the Rack application
104
- # @param res_body [Array] the body returned by the Rack application
105
- # @param lines [Puma::IOBuffer] modified in place
106
- # @param requests [Integer] number of inline requests handled
107
- # @param client [Puma::Client]
108
- # @return [Boolean,:async]
109
- def write_response(status, headers, res_body, lines, requests, client)
110
- env = client.env
111
- io = client.io
89
+ return :async
90
+ end
91
+ rescue ThreadPool::ForceShutdown => e
92
+ @events.unknown_error e, client, "Rack app"
93
+ @events.log "Detected force shutdown of a thread"
112
94
 
113
- return false if closed_socket?(io)
114
- lines.clear
95
+ status, headers, res_body = lowlevel_error(e, env, 503)
96
+ rescue Exception => e
97
+ @events.unknown_error e, client, "Rack app"
115
98
 
116
- head = env[REQUEST_METHOD] == HEAD
117
- after_reply = env[RACK_AFTER_REPLY] || []
99
+ status, headers, res_body = lowlevel_error(e, env, 500)
100
+ end
118
101
 
119
- begin
120
102
  res_info = {}
121
103
  res_info[:content_length] = nil
122
104
  res_info[:no_body] = head
@@ -167,9 +149,9 @@ module Puma
167
149
  res_body.each do |part|
168
150
  next if part.bytesize.zero?
169
151
  if chunked
170
- fast_write io, (part.bytesize.to_s(16) << line_ending)
171
- fast_write io, part # part may have different encoding
172
- fast_write io, line_ending
152
+ fast_write io, (part.bytesize.to_s(16) << line_ending)
153
+ fast_write io, part # part may have different encoding
154
+ fast_write io, line_ending
173
155
  else
174
156
  fast_write io, part
175
157
  end
@@ -185,11 +167,16 @@ module Puma
185
167
  end
186
168
 
187
169
  ensure
188
- uncork_socket io
189
-
190
- client.body.close if client.body
191
- client.tempfile.unlink if client.tempfile
192
- res_body.close if res_body.respond_to? :close
170
+ begin
171
+ uncork_socket io
172
+
173
+ body.close
174
+ client.tempfile.unlink if client.tempfile
175
+ ensure
176
+ # Whatever happens, we MUST call `close` on the response body.
177
+ # Otherwise Rack::BodyProxy callbacks may not fire and lead to various state leaks
178
+ res_body.close if res_body.respond_to? :close
179
+ end
193
180
 
194
181
  after_reply.each { |o| o.call }
195
182
  end
data/lib/puma/server.rb CHANGED
@@ -476,7 +476,7 @@ module Puma
476
476
  end
477
477
  true
478
478
  rescue StandardError => e
479
- client_error(e, client, buffer, requests)
479
+ client_error(e, client)
480
480
  # The ensure tries to close +client+ down
481
481
  requests > 0
482
482
  ensure
@@ -504,36 +504,37 @@ module Puma
504
504
  # :nocov:
505
505
 
506
506
  # Handle various error types thrown by Client I/O operations.
507
- def client_error(e, client, buffer = ::Puma::IOBuffer.new, requests = 1)
507
+ def client_error(e, client)
508
508
  # Swallow, do not log
509
509
  return if [ConnectionError, EOFError].include?(e.class)
510
510
 
511
+ lowlevel_error(e, client.env)
511
512
  case e
512
513
  when MiniSSL::SSLError
513
514
  @events.ssl_error e, client.io
514
515
  when HttpParserError
515
- status, headers, res_body = lowlevel_error(e, client.env, 400)
516
- write_response(status, headers, res_body, buffer, requests, client)
516
+ client.write_error(400)
517
+ @events.parse_error e, client
518
+ when HttpParserError501
519
+ client.write_error(501)
517
520
  @events.parse_error e, client
518
521
  else
519
- status, headers, res_body = lowlevel_error(e, client.env)
520
- write_response(status, headers, res_body, buffer, requests, client)
522
+ client.write_error(500)
521
523
  @events.unknown_error e, nil, "Read"
522
524
  end
523
525
  end
524
526
 
525
527
  # A fallback rack response if +@app+ raises as exception.
526
528
  #
527
- def lowlevel_error(e, env, status = 500)
529
+ def lowlevel_error(e, env, status=500)
528
530
  if handler = @options[:lowlevel_error_handler]
529
531
  if handler.arity == 1
530
- handler_status, headers, res_body = handler.call(e)
532
+ return handler.call(e)
531
533
  elsif handler.arity == 2
532
- handler_status, headers, res_body = handler.call(e, env)
534
+ return handler.call(e, env)
533
535
  else
534
- handler_status, headers, res_body = handler.call(e, env, status)
536
+ return handler.call(e, env, status)
535
537
  end
536
- return [handler_status || status, headers || {}, res_body || []]
537
538
  end
538
539
 
539
540
  if @leak_stack_on_error
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: puma
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.6.0
4
+ version: 5.6.4
5
5
  platform: java
6
6
  authors:
7
7
  - Evan Phoenix