Version in base suite: 5.6.5-3 Base version: puma_5.6.5-3 Target version: puma_5.6.5-3+deb12u1 Base file: /srv/ftp-master.debian.org/ftp/pool/main/p/puma/puma_5.6.5-3.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/p/puma/puma_5.6.5-3+deb12u1.dsc changelog | 19 ++++ patches/CVE-2023-40175.patch | 143 +++++++++++++++++++++++++++++++ patches/CVE-2024-21647.patch | 93 ++++++++++++++++++++ patches/CVE-2024-45614.patch | 195 +++++++++++++++++++++++++++++++++++++++++++ patches/series | 3 5 files changed, 453 insertions(+) diff -Nru puma-5.6.5/debian/changelog puma-5.6.5/debian/changelog --- puma-5.6.5/debian/changelog 2023-02-09 15:24:05.000000000 +0000 +++ puma-5.6.5/debian/changelog 2025-01-29 01:56:33.000000000 +0000 @@ -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 Wed, 29 Jan 2025 07:26:33 +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 00:00:00.000000000 +0000 +++ puma-5.6.5/debian/patches/CVE-2023-40175.patch 2024-12-31 05:34:27.000000000 +0000 @@ -0,0 +1,143 @@ +From 7405a219801dcebc0ad6e0aa108d4319ca23f662 Mon Sep 17 00:00:00 2001 +From: Nate Berkopec +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 +--- + 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 00:00:00.000000000 +0000 +++ puma-5.6.5/debian/patches/CVE-2024-21647.patch 2024-12-31 06:07:42.000000000 +0000 @@ -0,0 +1,93 @@ +From bbb880ffb6debbfdea535b4b3eb2204d49ae151d Mon Sep 17 00:00:00 2001 +From: Nate Berkopec +Date: Mon, 8 Jan 2024 14:48:43 +0900 +Subject: [PATCH] Merge pull request from GHSA-c2f4-cvqm-65w2 + +Co-authored-by: MSP-Greg +Co-authored-by: Patrik Ragnarsson +Co-authored-by: Evan Phoenix +--- + 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 00:00:00.000000000 +0000 +++ puma-5.6.5/debian/patches/CVE-2024-45614.patch 2024-12-31 08:30:04.000000000 +0000 @@ -0,0 +1,195 @@ +From cac3fd18cf29ed43719ff5d52d9cfec215f0a043 Mon Sep 17 00:00:00 2001 +From: Evan Phoenix +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 +--- + 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 15:24:05.000000000 +0000 +++ puma-5.6.5/debian/patches/series 2024-12-31 06:24:18.000000000 +0000 @@ -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