@@ -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 @@ class ConnectionError < RuntimeError; end
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
@@ -284,24 +300,40 @@ def setup_body
284
300
body = @parser . body
285
301
286
302
te = @env [ TRANSFER_ENCODING2 ]
287
-
288
303
if te
289
- if te . include? ( "," )
290
- te . split ( "," ) . each do |part |
291
- if CHUNKED . casecmp ( part . strip ) == 0
292
- return setup_chunked_body ( body )
293
- end
304
+ te_lwr = te . downcase
305
+ if te . include? ','
306
+ te_ary = te_lwr . split ','
307
+ te_count = te_ary . count CHUNKED
308
+ te_valid = te_ary [ 0 ..-2 ] . all? { |e | ALLOWED_TRANSFER_ENCODING . include? e }
309
+ if te_ary . last == CHUNKED && te_count == 1 && te_valid
310
+ @env . delete TRANSFER_ENCODING2
311
+ return setup_chunked_body body
312
+ elsif te_count >= 1
313
+ raise HttpParserError , "#{ TE_ERR_MSG } , multiple chunked: '#{ te } '"
314
+ elsif !te_valid
315
+ raise HttpParserError501 , "#{ TE_ERR_MSG } , unknown value: '#{ te } '"
294
316
end
295
- elsif CHUNKED . casecmp ( te ) == 0
296
- return setup_chunked_body ( body )
317
+ elsif te_lwr == CHUNKED
318
+ @env . delete TRANSFER_ENCODING2
319
+ return setup_chunked_body body
320
+ elsif ALLOWED_TRANSFER_ENCODING . include? te_lwr
321
+ raise HttpParserError , "#{ TE_ERR_MSG } , single value must be chunked: '#{ te } '"
322
+ else
323
+ raise HttpParserError501 , "#{ TE_ERR_MSG } , unknown value: '#{ te } '"
297
324
end
298
325
end
299
326
300
327
@chunked_body = false
301
328
302
329
cl = @env [ CONTENT_LENGTH ]
303
330
304
- unless cl
331
+ if cl
332
+ # cannot contain characters that are not \d
333
+ if cl =~ CONTENT_LENGTH_VALUE_INVALID
334
+ raise HttpParserError , "Invalid Content-Length: #{ cl . inspect } "
335
+ end
336
+ else
305
337
@buffer = body . empty? ? nil : body
306
338
@body = EmptyBody
307
339
set_ready
@@ -450,7 +482,13 @@ def decode_chunk(chunk)
450
482
while !io . eof?
451
483
line = io . gets
452
484
if line . end_with? ( "\r \n " )
453
- len = line . strip . to_i ( 16 )
485
+ # Puma doesn't process chunk extensions, but should parse if they're
486
+ # present, which is the reason for the semicolon regex
487
+ chunk_hex = line . strip [ /\A [^;]+/ ]
488
+ if chunk_hex =~ CHUNK_SIZE_INVALID
489
+ raise HttpParserError , "Invalid chunk size: '#{ chunk_hex } '"
490
+ end
491
+ len = chunk_hex . to_i ( 16 )
454
492
if len == 0
455
493
@in_last_chunk = true
456
494
@body . rewind
@@ -481,7 +519,12 @@ def decode_chunk(chunk)
481
519
482
520
case
483
521
when got == len
484
- write_chunk ( part [ 0 ..-3 ] ) # to skip the ending \r\n
522
+ # proper chunked segment must end with "\r\n"
523
+ if part . end_with? CHUNK_VALID_ENDING
524
+ write_chunk ( part [ 0 ..-3 ] ) # to skip the ending \r\n
525
+ else
526
+ raise HttpParserError , "Chunk size mismatch"
527
+ end
485
528
when got <= len - 2
486
529
write_chunk ( part )
487
530
@partial_part_left = len - part . size
0 commit comments