Skip to content

Commit cac3fd1

Browse files
evanphxdentarg
authored andcommitted
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 <[email protected]>
1 parent 5fc43d7 commit cac3fd1

File tree

5 files changed

+111
-3
lines changed

5 files changed

+111
-3
lines changed

ext/puma_http11/org/jruby/puma/Http11.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ public static void http_field(Ruby runtime, RubyHash req, ByteList buffer, int f
9999
int bite = b.get(i) & 0xFF;
100100
if(bite == '-') {
101101
b.set(i, (byte)'_');
102+
} else if(bite == '_') {
103+
b.set(i, (byte)',');
102104
} else {
103105
b.set(i, (byte)Character.toUpperCase(bite));
104106
}

lib/puma/const.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,14 @@ module Const
281281
# header values can contain HTAB?
282282
ILLEGAL_HEADER_VALUE_REGEX = /[\x00-\x08\x0A-\x1F]/.freeze
283283

284+
# The keys of headers that should not be convert to underscore
285+
# normalized versions. These headers are ignored at the request reading layer,
286+
# but if we normalize them after reading, it's just confusing for the application.
287+
UNMASKABLE_HEADERS = {
288+
"HTTP_TRANSFER,ENCODING" => true,
289+
"HTTP_CONTENT,LENGTH" => true,
290+
}
291+
284292
# Banned keys of response header
285293
BANNED_HEADER_KEY = /\A(rack\.|status\z)/.freeze
286294

lib/puma/request.rb

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,11 @@ def illegal_header_value?(header_value)
495495
# compatibility, we'll convert them back. This code is written to
496496
# avoid allocation in the common case (ie there are no headers
497497
# with `,` in their names), that's why it has the extra conditionals.
498+
#
499+
# @note If a normalized version of a `,` header already exists, we ignore
500+
# the `,` version. This prevents clobbering headers managed by proxies
501+
# but not by clients (Like X-Forwarded-For).
502+
#
498503
# @param env [Hash] see Puma::Client#env, from request, modifies in place
499504
# @version 5.0.3
500505
#
@@ -503,23 +508,31 @@ def req_env_post_parse(env)
503508
to_add = nil
504509

505510
env.each do |k,v|
506-
if k.start_with?("HTTP_") && k.include?(",") && k != "HTTP_TRANSFER,ENCODING"
511+
if k.start_with?("HTTP_") && k.include?(",") && !UNMASKABLE_HEADERS.key?(k)
507512
if to_delete
508513
to_delete << k
509514
else
510515
to_delete = [k]
511516
end
512517

518+
new_k = k.tr(",", "_")
519+
if env.key?(new_k)
520+
next
521+
end
522+
513523
unless to_add
514524
to_add = {}
515525
end
516526

517-
to_add[k.tr(",", "_")] = v
527+
to_add[new_k] = v
518528
end
519529
end
520530

521-
if to_delete
531+
if to_delete # rubocop:disable Style/SafeNavigation
522532
to_delete.each { |k| env.delete(k) }
533+
end
534+
535+
if to_add
523536
env.merge! to_add
524537
end
525538
end

test/test_normalize.rb

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# frozen_string_literal: true
2+
3+
require_relative "helper"
4+
5+
require "puma/request"
6+
7+
class TestNormalize < Minitest::Test
8+
parallelize_me!
9+
10+
include Puma::Request
11+
12+
def test_comma_headers
13+
env = {
14+
"HTTP_X_FORWARDED_FOR" => "1.1.1.1",
15+
"HTTP_X_FORWARDED,FOR" => "2.2.2.2",
16+
}
17+
18+
req_env_post_parse env
19+
20+
expected = {
21+
"HTTP_X_FORWARDED_FOR" => "1.1.1.1",
22+
}
23+
24+
assert_equal expected, env
25+
26+
# Test that the iteration order doesn't matter
27+
28+
env = {
29+
"HTTP_X_FORWARDED,FOR" => "2.2.2.2",
30+
"HTTP_X_FORWARDED_FOR" => "1.1.1.1",
31+
}
32+
33+
req_env_post_parse env
34+
35+
expected = {
36+
"HTTP_X_FORWARDED_FOR" => "1.1.1.1",
37+
}
38+
39+
assert_equal expected, env
40+
end
41+
42+
def test_unmaskable_headers
43+
env = {
44+
"HTTP_CONTENT,LENGTH" => "100000",
45+
"HTTP_TRANSFER,ENCODING" => "chunky"
46+
}
47+
48+
req_env_post_parse env
49+
50+
expected = {
51+
"HTTP_CONTENT,LENGTH" => "100000",
52+
"HTTP_TRANSFER,ENCODING" => "chunky"
53+
}
54+
55+
assert_equal expected, env
56+
end
57+
end

test/test_request_invalid.rb

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,4 +215,32 @@ def test_chunked_size_mismatch_2
215215

216216
assert_status data
217217
end
218+
219+
def test_underscore_header_1
220+
hdrs = [
221+
"X-FORWARDED-FOR: 1.1.1.1", # proper
222+
"X-FORWARDED-FOR: 2.2.2.2", # proper
223+
"X_FORWARDED-FOR: 3.3.3.3", # invalid, contains underscore
224+
"Content-Length: 5",
225+
].join "\r\n"
226+
227+
response = send_http_and_read "#{GET_PREFIX}#{hdrs}\r\n\r\nHello\r\n\r\n"
228+
229+
assert_includes response, "HTTP_X_FORWARDED_FOR = 1.1.1.1, 2.2.2.2"
230+
refute_includes response, "3.3.3.3"
231+
end
232+
233+
def test_underscore_header_2
234+
hdrs = [
235+
"X_FORWARDED-FOR: 3.3.3.3", # invalid, contains underscore
236+
"X-FORWARDED-FOR: 2.2.2.2", # proper
237+
"X-FORWARDED-FOR: 1.1.1.1", # proper
238+
"Content-Length: 5",
239+
].join "\r\n"
240+
241+
response = send_http_and_read "#{GET_PREFIX}#{hdrs}\r\n\r\nHello\r\n\r\n"
242+
243+
assert_includes response, "HTTP_X_FORWARDED_FOR = 2.2.2.2, 1.1.1.1"
244+
refute_includes response, "3.3.3.3"
245+
end
218246
end

0 commit comments

Comments
 (0)