[DRE-maint] Bug#1091795: bookworm-pu: package puma/5.6.5-3+deb12u1

Abhijith PA abhijith at debian.org
Tue Dec 31 14:12:35 GMT 2024


Package: release.debian.org
Severity: normal
Tags: bookworm
User: release.debian.org at packages.debian.org
Usertags: pu
X-Debbugs-Cc: puma at packages.debian.org
Control: affects -1 + src:puma

Dear stable release team,

[ Reason ]
As a ruby team member I would like to do a update of this package in
stable to fix CVE-2023-40175,CVE-2024-21647 and CVE-2024-45614.

[ Tests ]
All tests from upstream related to this CVE is also backported and ran
successfully along with the existing tests.


[ Checklist ]
  [x] *all* changes are documented in the d/changelog
  [x] I reviewed all changes and I approve them
  [x] attach debdiff against the package in stable
  [x] the issue is verified as fixed in unstable

[ Changes ]
Debdiff are attached.
Except some minor corrections, all changes fitted without any fuzz.
-------------- next part --------------
diff -Nru puma-5.6.5/debian/changelog puma-5.6.5/debian/changelog
--- puma-5.6.5/debian/changelog	2023-02-09 20:54:05.000000000 +0530
+++ puma-5.6.5/debian/changelog	2024-12-31 17:44:27.000000000 +0530
@@ -1,3 +1,22 @@
+puma (5.6.5-3+deb12u1) bookworm; urgency=medium
+
+  * Team upload
+  * d/patches/
+   + CVE-2023-40175.patch: Fix CVE-2023-40175, incorrect behavior when
+     parsing chunked transfer encoding bodies and zero-length
+     Content-Length headers in a way that allowed HTTP request
+     smuggling. (Closes: #1050079)
+
+   + CVE-2024-21647.patch: Fix CVE-2024-21647 by limiting the size of
+     chunk extensions. (Closes: #1060345)
+
+   + CVE-2024-45614.patch: Fix CVE-2024-45614, clients could clobber
+     values set by intermediate proxies (such as X-Forwarded-For) by
+     providing a underscore version of the same header.
+     (Closes: #1082379)
+
+ -- Abhijith PA <abhijith at debian.org>  Tue, 31 Dec 2024 17:44:27 +0530
+
 puma (5.6.5-3) unstable; urgency=medium
 
   * Team upload.
diff -Nru puma-5.6.5/debian/patches/CVE-2023-40175.patch puma-5.6.5/debian/patches/CVE-2023-40175.patch
--- puma-5.6.5/debian/patches/CVE-2023-40175.patch	1970-01-01 05:30:00.000000000 +0530
+++ puma-5.6.5/debian/patches/CVE-2023-40175.patch	2024-12-31 11:04:27.000000000 +0530
@@ -0,0 +1,143 @@
+From 7405a219801dcebc0ad6e0aa108d4319ca23f662 Mon Sep 17 00:00:00 2001
+From: Nate Berkopec <nate.berkopec at gmail.com>
+Date: Fri, 18 Aug 2023 09:47:23 +0900
+Subject: [PATCH] Merge pull request from GHSA-68xg-gqqm-vgj8
+
+* 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 at 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
diff -Nru puma-5.6.5/debian/patches/CVE-2024-21647.patch puma-5.6.5/debian/patches/CVE-2024-21647.patch
--- puma-5.6.5/debian/patches/CVE-2024-21647.patch	1970-01-01 05:30:00.000000000 +0530
+++ puma-5.6.5/debian/patches/CVE-2024-21647.patch	2024-12-31 11:37:42.000000000 +0530
@@ -0,0 +1,93 @@
+From bbb880ffb6debbfdea535b4b3eb2204d49ae151d Mon Sep 17 00:00:00 2001
+From: Nate Berkopec <nate.berkopec at gmail.com>
+Date: Mon, 8 Jan 2024 14:48:43 +0900
+Subject: [PATCH] Merge pull request from GHSA-c2f4-cvqm-65w2
+
+Co-authored-by: MSP-Greg <MSP-Greg at users.noreply.github.com>
+Co-authored-by: Patrik Ragnarsson <patrik at starkast.net>
+Co-authored-by: Evan Phoenix <evan at phx.io>
+---
+ lib/puma/client.rb       | 27 +++++++++++++++++++++++++++
+ test/test_puma_server.rb | 14 ++++++++++++++
+ 2 files changed, 41 insertions(+)
+
+--- a/lib/puma/client.rb
++++ b/lib/puma/client.rb
+@@ -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
+--- a/test/test_puma_server.rb
++++ b/test/test_puma_server.rb
+@@ -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
diff -Nru puma-5.6.5/debian/patches/CVE-2024-45614.patch puma-5.6.5/debian/patches/CVE-2024-45614.patch
--- puma-5.6.5/debian/patches/CVE-2024-45614.patch	1970-01-01 05:30:00.000000000 +0530
+++ puma-5.6.5/debian/patches/CVE-2024-45614.patch	2024-12-31 14:00:04.000000000 +0530
@@ -0,0 +1,195 @@
+From cac3fd18cf29ed43719ff5d52d9cfec215f0a043 Mon Sep 17 00:00:00 2001
+From: Evan Phoenix <evan at phx.io>
+Date: Wed, 18 Sep 2024 21:56:07 -0700
+Subject: [PATCH] Merge commit from fork
+
+* Prevent underscores from clobbering hyphen headers
+
+* Special case encoding headers to prevent app confusion
+
+* Handle _ as , in jruby as well
+
+* Silence RuboCop offense
+
+---------
+
+Co-authored-by: Patrik Ragnarsson <patrik at starkast.net>
+---
+ ext/puma_http11/org/jruby/puma/Http11.java |  2 +
+ lib/puma/const.rb                          |  8 +++
+ lib/puma/request.rb                        | 19 ++++++--
+ test/test_normalize.rb                     | 57 ++++++++++++++++++++++
+ test/test_request_invalid.rb               | 28 +++++++++++
+ 5 files changed, 111 insertions(+), 3 deletions(-)
+ create mode 100644 test/test_normalize.rb
+
+--- a/ext/puma_http11/org/jruby/puma/Http11.java
++++ b/ext/puma_http11/org/jruby/puma/Http11.java
+@@ -99,6 +99,8 @@ public class Http11 extends RubyObject {
+             int bite = b.get(i) & 0xFF;
+             if(bite == '-') {
+                 b.set(i, (byte)'_');
++            } else if(bite == '_') {
++                b.set(i, (byte)',');
+             } else {
+                 b.set(i, (byte)Character.toUpperCase(bite));
+             }
+--- a/lib/puma/const.rb
++++ b/lib/puma/const.rb
+@@ -244,6 +244,14 @@ module Puma
+     # header values can contain HTAB?
+     ILLEGAL_HEADER_VALUE_REGEX = /[\x00-\x08\x0A-\x1F]/.freeze
+ 
++    # The keys of headers that should not be convert to underscore
++    # normalized versions. These headers are ignored at the request reading layer,
++    # but if we normalize them after reading, it's just confusing for the application.
++    UNMASKABLE_HEADERS = {
++      "HTTP_TRANSFER,ENCODING" => true,
++      "HTTP_CONTENT,LENGTH" => true,
++    }
++
+     # Banned keys of response header
+     BANNED_HEADER_KEY = /\A(rack\.|status\z)/.freeze
+ 
+--- a/lib/puma/request.rb
++++ b/lib/puma/request.rb
+@@ -318,6 +318,11 @@ module Puma
+     # compatibility, we'll convert them back. This code is written to
+     # avoid allocation in the common case (ie there are no headers
+     # with `,` in their names), that's why it has the extra conditionals.
++    #
++    # @note If a normalized version of a `,` header already exists, we ignore
++    #       the `,` version. This prevents clobbering headers managed by proxies
++    #       but not by clients (Like X-Forwarded-For).
++    #
+     # @param env [Hash] see Puma::Client#env, from request, modifies in place
+     # @version 5.0.3
+     #
+@@ -326,23 +331,30 @@ module Puma
+       to_add = nil
+ 
+       env.each do |k,v|
+-        if k.start_with?("HTTP_") and k.include?(",") and k != "HTTP_TRANSFER,ENCODING"
++        if k.start_with?("HTTP_") and k.include?(",") and !UNMASKABLE_HEADERS.key?(k)
+           if to_delete
+             to_delete << k
+           else
+             to_delete = [k]
+           end
+ 
++          new_k = k.tr(",", "_")
++          if env.key?(new_k)
++            next
++          end
++
+           unless to_add
+             to_add = {}
+           end
+ 
+-          to_add[k.tr(",", "_")] = v
++          to_add[new_k] = v
+         end
+       end
+ 
+       if to_delete
+         to_delete.each { |k| env.delete(k) }
++      end
++      if to_add
+         env.merge! to_add
+       end
+     end
+--- /dev/null
++++ b/test/test_normalize.rb
+@@ -0,0 +1,57 @@
++# frozen_string_literal: true
++
++require_relative "helper"
++
++require "puma/request"
++
++class TestNormalize < Minitest::Test
++  parallelize_me!
++
++  include Puma::Request
++
++  def test_comma_headers
++    env = {
++      "HTTP_X_FORWARDED_FOR" => "1.1.1.1",
++      "HTTP_X_FORWARDED,FOR" => "2.2.2.2",
++    }
++
++    req_env_post_parse env
++
++    expected = {
++      "HTTP_X_FORWARDED_FOR" => "1.1.1.1",
++    }
++
++    assert_equal expected, env
++
++    # Test that the iteration order doesn't matter
++
++    env = {
++      "HTTP_X_FORWARDED,FOR" => "2.2.2.2",
++      "HTTP_X_FORWARDED_FOR" => "1.1.1.1",
++    }
++
++    req_env_post_parse env
++
++    expected = {
++      "HTTP_X_FORWARDED_FOR" => "1.1.1.1",
++    }
++
++    assert_equal expected, env
++  end
++
++  def test_unmaskable_headers
++    env = {
++      "HTTP_CONTENT,LENGTH" => "100000",
++      "HTTP_TRANSFER,ENCODING" => "chunky"
++    }
++
++    req_env_post_parse env
++
++    expected = {
++      "HTTP_CONTENT,LENGTH" => "100000",
++      "HTTP_TRANSFER,ENCODING" => "chunky"
++    }
++
++    assert_equal expected, env
++  end
++end
+--- a/test/test_request_invalid.rb
++++ b/test/test_request_invalid.rb
+@@ -216,4 +216,32 @@ class TestRequestInvalid < Minitest::Tes
+ 
+     assert_status data
+   end
++
++  def test_underscore_header_1
++    hdrs = [
++      "X-FORWARDED-FOR: 1.1.1.1",  # proper
++      "X-FORWARDED-FOR: 2.2.2.2",  # proper
++      "X_FORWARDED-FOR: 3.3.3.3",  # invalid, contains underscore
++      "Content-Length: 5",
++    ].join "\r\n"
++
++    response = send_http_and_read "#{GET_PREFIX}#{hdrs}\r\n\r\nHello\r\n\r\n"
++
++    assert_includes response, "HTTP_X_FORWARDED_FOR = 1.1.1.1, 2.2.2.2"
++    refute_includes response, "3.3.3.3"
++  end
++
++  def test_underscore_header_2
++    hdrs = [
++      "X_FORWARDED-FOR: 3.3.3.3",  # invalid, contains underscore
++      "X-FORWARDED-FOR: 2.2.2.2",  # proper
++      "X-FORWARDED-FOR: 1.1.1.1",  # proper
++      "Content-Length: 5",
++    ].join "\r\n"
++
++    response = send_http_and_read "#{GET_PREFIX}#{hdrs}\r\n\r\nHello\r\n\r\n"
++
++    assert_includes response, "HTTP_X_FORWARDED_FOR = 2.2.2.2, 1.1.1.1"
++    refute_includes response, "3.3.3.3"
++  end
+ end
diff -Nru puma-5.6.5/debian/patches/series puma-5.6.5/debian/patches/series
--- puma-5.6.5/debian/patches/series	2023-02-09 20:54:05.000000000 +0530
+++ puma-5.6.5/debian/patches/series	2024-12-31 11:54:18.000000000 +0530
@@ -3,3 +3,6 @@
 0012-disable-cli-ssl-tests.patch
 0013-fix-test-term-not-accepts-new-connections.patch
 0014-disable-test-failing-on-amd64.patch
+CVE-2023-40175.patch
+CVE-2024-21647.patch
+CVE-2024-45614.patch


More information about the Pkg-ruby-extras-maintainers mailing list