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 +4 -4
- data/History.md +16 -0
- data/lib/puma/client.rb +54 -11
- data/lib/puma/const.rb +6 -4
- data/lib/puma/puma_http11.jar +0 -0
- data/lib/puma/request.rb +38 -51
- data/lib/puma/server.rb +12 -11
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 00fb5299edf47cd69e241fc63a66cb98ef7180e0df430651bab94adeec19bc9f
|
4
|
+
data.tar.gz: 70089c146221037a050001dbeca34da0be535268994214eace5d9f1c670684b6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
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
|
314
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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.
|
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
|
data/lib/puma/puma_http11.jar
CHANGED
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
|
-
|
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
|
-
|
72
|
-
@
|
73
|
-
|
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
|
-
|
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
|
-
|
98
|
-
|
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
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
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
|
-
|
114
|
-
|
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
|
-
|
117
|
-
|
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
|
-
|
171
|
-
|
172
|
-
|
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
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
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
|
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
|
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
|
-
|
516
|
-
|
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
|
-
|
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
|
529
|
+
def lowlevel_error(e, env, status=500)
|
528
530
|
if handler = @options[:lowlevel_error_handler]
|
529
531
|
if handler.arity == 1
|
530
|
-
|
532
|
+
return handler.call(e)
|
531
533
|
elsif handler.arity == 2
|
532
|
-
|
534
|
+
return handler.call(e, env)
|
533
535
|
else
|
534
|
-
|
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
|