[PATCH] Merge pull request from GHSA-c2f4-cvqm-65w2
authorNate Berkopec <nate.berkopec@gmail.com>
Mon, 8 Jan 2024 05:48:43 +0000 (14:48 +0900)
committerAbhijith PA <abhijith@debian.org>
Wed, 29 Jan 2025 01:56:33 +0000 (07:26 +0530)
Co-authored-by: MSP-Greg <MSP-Greg@users.noreply.github.com>
Co-authored-by: Patrik Ragnarsson <patrik@starkast.net>
Co-authored-by: Evan Phoenix <evan@phx.io>
Gbp-Pq: Name CVE-2024-21647.patch

lib/puma/client.rb
test/test_puma_server.rb

index 9c11912caac82c582f106432622f6f790d475f1e..b5a1569c6869eeab7756261eb0a4769534645008 100644 (file)
@@ -48,6 +48,14 @@ module Puma
     CHUNK_VALID_ENDING = Const::LINE_END
     CHUNK_VALID_ENDING_SIZE = CHUNK_VALID_ENDING.bytesize
 
+    # The maximum number of bytes we'll buffer looking for a valid
+    # chunk header.
+    MAX_CHUNK_HEADER_SIZE = 4096
+
+    # The maximum amount of excess data the client sends
+    # using chunk size extensions before we abort the connection.
+    MAX_CHUNK_EXCESS = 16 * 1024
+
     # Content-Length header value validation
     CONTENT_LENGTH_VALUE_INVALID = /[^\d]/.freeze
 
@@ -460,6 +468,7 @@ module Puma
       @chunked_body = true
       @partial_part_left = 0
       @prev_chunk = ""
+      @excess_cr = 0
 
       @body = Tempfile.new(Const::PUMA_TMP_BASE)
       @body.unlink
@@ -541,6 +550,20 @@ module Puma
             end
           end
 
+          # Track the excess as a function of the size of the
+          # header vs the size of the actual data. Excess can
+          # go negative (and is expected to) when the body is
+          # significant.
+          # The additional of chunk_hex.size and 2 compensates
+          # for a client sending 1 byte in a chunked body over
+          # a long period of time, making sure that that client
+          # isn't accidentally eventually punished.
+          @excess_cr += (line.size - len - chunk_hex.size - 2)
+
+          if @excess_cr >= MAX_CHUNK_EXCESS
+            raise HttpParserError, "Maximum chunk excess detected"
+          end
+
           len += 2
 
           part = io.read(len)
@@ -568,6 +591,10 @@ module Puma
             @partial_part_left = len - part.size
           end
         else
+          if @prev_chunk.size + chunk.size >= MAX_CHUNK_HEADER_SIZE
+            raise HttpParserError, "maximum size of chunk header exceeded"
+          end
+
           @prev_chunk = line
           return false
         end
index eb135bde7c1fdb99411cee17ff2f2cb860d3f7d7..81bf23e7144d1c4530ecc04e103bcba9a0003e30 100644 (file)
@@ -648,6 +648,20 @@ EOF
     end
   end
 
+  def test_large_chunked_request_header
+    server_run(environment: :production) { |env|
+      [200, {}, [""]]
+    }
+
+    max_chunk_header_size = Puma::Client::MAX_CHUNK_HEADER_SIZE
+    header = "GET / HTTP/1.1\r\nConnection: close\r\nContent-Length: 200\r\nTransfer-Encoding: chunked\r\n\r\n"
+    socket = send_http "#{header}1;t#{'x' * (max_chunk_header_size + 2)}"
+
+    data = socket.read
+
+    assert_match "HTTP/1.1 400 Bad Request\r\n\r\n", data
+  end
+
   def test_chunked_request_pause_before_value
     body = nil
     content_length = nil