147 lines
5.3 KiB
Diff
147 lines
5.3 KiB
Diff
From 7405a219801dcebc0ad6e0aa108d4319ca23f662 Mon Sep 17 00:00:00 2001
|
|
From: Nate Berkopec <nate.berkopec@gmail.com>
|
|
Date: Fri, 18 Aug 2023 09:47:23 +0900
|
|
Subject: [PATCH] Merge pull request from GHSA-68xg-gqqm-vgj8
|
|
|
|
Origin: https://github.com/puma/puma/commit/7405a219801dcebc0ad6e0aa108d4319ca23f662
|
|
|
|
* Reject empty string for Content-Length
|
|
|
|
* Ignore trailers in last chunk
|
|
|
|
* test_puma_server.rb - use heredoc, test_cl_and_te_smuggle
|
|
|
|
* client.rb - stye/RubyCop
|
|
|
|
* test_puma_server.rb - indented heredoc rubocop disable
|
|
|
|
* Dentarg comments
|
|
|
|
* Remove unused variable
|
|
|
|
---------
|
|
|
|
Co-authored-by: MSP-Greg <Greg.mpls@gmail.com>
|
|
---
|
|
lib/puma/client.rb | 23 ++++++++++++++--------
|
|
test/test_puma_server.rb | 42 +++++++++++++++++++++++++++++++++++++++-
|
|
2 files changed, 56 insertions(+), 9 deletions(-)
|
|
|
|
diff --git a/lib/puma/client.rb b/lib/puma/client.rb
|
|
index e966f995e8..9c11912caa 100644
|
|
--- a/lib/puma/client.rb
|
|
+++ b/lib/puma/client.rb
|
|
@@ -45,7 +45,8 @@ class Client
|
|
|
|
# chunked body validation
|
|
CHUNK_SIZE_INVALID = /[^\h]/.freeze
|
|
- CHUNK_VALID_ENDING = "\r\n".freeze
|
|
+ CHUNK_VALID_ENDING = Const::LINE_END
|
|
+ CHUNK_VALID_ENDING_SIZE = CHUNK_VALID_ENDING.bytesize
|
|
|
|
# Content-Length header value validation
|
|
CONTENT_LENGTH_VALUE_INVALID = /[^\d]/.freeze
|
|
@@ -347,8 +348,8 @@ def setup_body
|
|
cl = @env[CONTENT_LENGTH]
|
|
|
|
if cl
|
|
- # cannot contain characters that are not \d
|
|
- if cl =~ CONTENT_LENGTH_VALUE_INVALID
|
|
+ # cannot contain characters that are not \d, or be empty
|
|
+ if cl =~ CONTENT_LENGTH_VALUE_INVALID || cl.empty?
|
|
raise HttpParserError, "Invalid Content-Length: #{cl.inspect}"
|
|
end
|
|
else
|
|
@@ -509,7 +510,7 @@ def decode_chunk(chunk)
|
|
|
|
while !io.eof?
|
|
line = io.gets
|
|
- if line.end_with?("\r\n")
|
|
+ if line.end_with?(CHUNK_VALID_ENDING)
|
|
# Puma doesn't process chunk extensions, but should parse if they're
|
|
# present, which is the reason for the semicolon regex
|
|
chunk_hex = line.strip[/\A[^;]+/]
|
|
@@ -521,13 +522,19 @@ def decode_chunk(chunk)
|
|
@in_last_chunk = true
|
|
@body.rewind
|
|
rest = io.read
|
|
- last_crlf_size = "\r\n".bytesize
|
|
- if rest.bytesize < last_crlf_size
|
|
+ if rest.bytesize < CHUNK_VALID_ENDING_SIZE
|
|
@buffer = nil
|
|
- @partial_part_left = last_crlf_size - rest.bytesize
|
|
+ @partial_part_left = CHUNK_VALID_ENDING_SIZE - rest.bytesize
|
|
return false
|
|
else
|
|
- @buffer = rest[last_crlf_size..-1]
|
|
+ # if the next character is a CRLF, set buffer to everything after that CRLF
|
|
+ start_of_rest = if rest.start_with?(CHUNK_VALID_ENDING)
|
|
+ CHUNK_VALID_ENDING_SIZE
|
|
+ else # we have started a trailer section, which we do not support. skip it!
|
|
+ rest.index(CHUNK_VALID_ENDING*2) + CHUNK_VALID_ENDING_SIZE*2
|
|
+ end
|
|
+
|
|
+ @buffer = rest[start_of_rest..-1]
|
|
@buffer = nil if @buffer.empty?
|
|
set_ready
|
|
return true
|
|
diff --git a/test/test_puma_server.rb b/test/test_puma_server.rb
|
|
index 298e44b439..2bfaf98848 100644
|
|
--- a/test/test_puma_server.rb
|
|
+++ b/test/test_puma_server.rb
|
|
@@ -627,7 +627,7 @@ def test_large_chunked_request
|
|
[200, {}, [""]]
|
|
}
|
|
|
|
- header = "GET / HTTP/1.1\r\nConnection: close\r\nTransfer-Encoding: chunked\r\n\r\n"
|
|
+ header = "GET / HTTP/1.1\r\nConnection: close\r\nContent-Length: 200\r\nTransfer-Encoding: chunked\r\n\r\n"
|
|
|
|
chunk_header_size = 6 # 4fb8\r\n
|
|
# Current implementation reads one chunk of CHUNK_SIZE, then more chunks of size 4096.
|
|
@@ -1365,4 +1365,44 @@ def test_rack_url_scheme_user
|
|
data = send_http_and_read "GET / HTTP/1.0\r\n\r\n"
|
|
assert_equal "user", data.split("\r\n").last
|
|
end
|
|
+
|
|
+ def test_cl_empty_string
|
|
+ server_run do |env|
|
|
+ [200, {}, [""]]
|
|
+ end
|
|
+
|
|
+ empty_cl_request = "GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length:\r\n\r\nGET / HTTP/1.1\r\nHost: localhost\r\n\r\n"
|
|
+
|
|
+ data = send_http_and_read empty_cl_request
|
|
+ assert_operator data, :start_with?, 'HTTP/1.1 400 Bad Request'
|
|
+ end
|
|
+
|
|
+ def test_crlf_trailer_smuggle
|
|
+ server_run do |env|
|
|
+ [200, {}, [""]]
|
|
+ end
|
|
+
|
|
+ smuggled_payload = "GET / HTTP/1.1\r\nTransfer-Encoding: chunked\r\nHost: whatever\r\n\r\n0\r\nX:POST / HTTP/1.1\r\nHost: whatever\r\n\r\nGET / HTTP/1.1\r\nHost: whatever\r\n\r\n"
|
|
+
|
|
+ data = send_http_and_read smuggled_payload
|
|
+ assert_equal 2, data.scan("HTTP/1.1 200 OK").size
|
|
+ end
|
|
+
|
|
+ # test to check if content-length is ignored when 'transfer-encoding: chunked'
|
|
+ # is used. See also test_large_chunked_request
|
|
+ def test_cl_and_te_smuggle
|
|
+ body = nil
|
|
+ server_run { |env|
|
|
+ body = env['rack.input'].read
|
|
+ [200, {}, [""]]
|
|
+ }
|
|
+
|
|
+ req = "POST /search HTTP/1.1\r\nHost: vulnerable-website.com\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 4\r\nTransfer-Encoding: chunked\r\n\r\n7b\r\nGET /404 HTTP/1.1\r\nHost: vulnerable-website.com\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 144\r\n\r\nx=\r\n0\r\n\r\n"
|
|
+
|
|
+ data = send_http_and_read req
|
|
+
|
|
+ assert_includes body, "GET /404 HTTP/1.1\r\n"
|
|
+ assert_includes body, "Content-Length: 144\r\n"
|
|
+ assert_equal 1, data.scan("HTTP/1.1 200 OK").size
|
|
+ end
|
|
end
|
|
|