Skip to content

Commit

Permalink
Merge commit from fork
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
2 people authored and nateberkopec committed Sep 19, 2024
1 parent 5fc43d7 commit cac3fd1
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 3 deletions.
2 changes: 2 additions & 0 deletions ext/puma_http11/org/jruby/puma/Http11.java
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ public static void http_field(Ruby runtime, RubyHash req, ByteList buffer, int f
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));
}
Expand Down
8 changes: 8 additions & 0 deletions lib/puma/const.rb
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,14 @@ module Const
# 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

Expand Down
19 changes: 16 additions & 3 deletions lib/puma/request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,11 @@ def illegal_header_value?(header_value)
# 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
#
Expand All @@ -503,23 +508,31 @@ def req_env_post_parse(env)
to_add = nil

env.each do |k,v|
if k.start_with?("HTTP_") && k.include?(",") && k != "HTTP_TRANSFER,ENCODING"
if k.start_with?("HTTP_") && k.include?(",") && !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
if to_delete # rubocop:disable Style/SafeNavigation
to_delete.each { |k| env.delete(k) }
end

if to_add
env.merge! to_add
end
end
Expand Down
57 changes: 57 additions & 0 deletions test/test_normalize.rb
Original file line number Diff line number Diff line change
@@ -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
28 changes: 28 additions & 0 deletions test/test_request_invalid.rb
Original file line number Diff line number Diff line change
Expand Up @@ -215,4 +215,32 @@ def test_chunked_size_mismatch_2

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

0 comments on commit cac3fd1

Please sign in to comment.