[PATCH] Merge commit from fork
authorEvan Phoenix <evan@phx.io>
Thu, 19 Sep 2024 04:56:07 +0000 (21:56 -0700)
committerAbhijith PA <abhijith@debian.org>
Wed, 29 Jan 2025 01:56:33 +0000 (07:26 +0530)
* 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@starkast.net>
Gbp-Pq: Name CVE-2024-45614.patch

ext/puma_http11/org/jruby/puma/Http11.java
lib/puma/const.rb
lib/puma/request.rb
test/test_normalize.rb [new file with mode: 0644]
test/test_request_invalid.rb

index cd7a5d3bb0f9dcf813a139ee19ce5f7632d1e325..0c4f79eee7a2c36193a47df59fa9ff2a0fbe2b13 100644 (file)
@@ -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));
             }
index cc0fe95fbd63d3731354a8dceb478ef032311bae..12b8bfd59370cf8c563a9346c666f19c677b75af 100644 (file)
@@ -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
 
index 8c7b008ee82723e2753017e3badb2e923b7ea158..bedabe3481132a565dd4aa9c48692d71917aa486 100644 (file)
@@ -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
diff --git a/test/test_normalize.rb b/test/test_normalize.rb
new file mode 100644 (file)
index 0000000..60e61c3
--- /dev/null
@@ -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
index 8e9295b59216a90a677aaf144d1c195216cc6fb8..c6aa91ab0570909bccf81aae5886087b9ad15126 100644 (file)
@@ -216,4 +216,32 @@ class TestRequestInvalid < Minitest::Test
 
     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