Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion History.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@

* Bugfixes
* Your bugfix goes here <Most recent on the top, like GitHub> (#Github Number)

* Ignore illegal (by Rack spec) response header (#2439)

## 5.0.3 / 2020-10-26

* Bugfixes
Expand Down
9 changes: 8 additions & 1 deletion lib/puma/const.rb
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,6 @@ module Const
COLON = ": ".freeze

NEWLINE = "\n".freeze
HTTP_INJECTION_REGEX = /[\r\n]/.freeze

HIJACK_P = "rack.hijack?".freeze
HIJACK = "rack.hijack".freeze
Expand All @@ -239,5 +238,13 @@ module Const
# Mininum interval to checks worker health
WORKER_CHECK_INTERVAL = 5

# Illegal character in the key or value of response header
DQUOTE = "\"".freeze
HTTP_HEADER_DELIMITER = Regexp.escape("(),/:;<=>?@[]{}").freeze
ILLEGAL_HEADER_KEY_REGEX = /[\u0000-\u0025|#{DQUOTE}|#{HTTP_HEADER_DELIMITER}]/.freeze
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regex blocks valid HTTP header token characters

Medium Severity

The ILLEGAL_HEADER_KEY_REGEX uses range \u0000-\u0025 which blocks characters 0-37 decimal. This incorrectly blocks valid RFC 7230 token characters ! (33), # (35), $ (36), and % (37). Per RFC 7230, these are explicitly valid tchar characters. Valid response headers containing these characters would be incorrectly filtered out. The range likely intended to be \u0000-\u0020 (control chars + space) with DQUOTE added separately.

Fix in Cursor Fix in Web

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regex incorrectly blocks valid pipe character in headers

Medium Severity

The ILLEGAL_HEADER_KEY_REGEX contains literal | characters inside the character class brackets, which are treated as literal matches rather than alternation operators. This causes headers containing | in their names to be incorrectly filtered. Per RFC 7230, | is a valid tchar character allowed in HTTP header tokens. The author likely intended alternation but | inside [] is literal in regex.

Fix in Cursor Fix in Web

ILLEGAL_HEADER_VALUE_REGEX = /[\000-\037]/.freeze

# Banned keys of response header
BANNED_HEADER_KEY = /rack.|status/.freeze
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Banned header regex matches unintended header names

Medium Severity

The BANNED_HEADER_KEY regex /rack.|status/ has two issues: the . after rack is unescaped and matches any character (so "racka", "rack1" would match), and status lacks anchors so it matches substrings (so "x-status-code", "mystatus" would incorrectly match). The regex likely intended /^rack\.|^status$/i to match headers starting with "rack." or exactly equal to "status".

Fix in Cursor Fix in Web

end
end
25 changes: 19 additions & 6 deletions lib/puma/request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -282,13 +282,20 @@ def normalize_env(env, client)
end
# private :normalize_env

# @param header_key [#to_s]
# @return [Boolean]
#
def illegal_header_key?(header_key)
!!(ILLEGAL_HEADER_KEY_REGEX =~ header_key.to_s)
end

# @param header_value [#to_s]
# @return [Boolean]
#
def possible_header_injection?(header_value)
!!(HTTP_INJECTION_REGEX =~ header_value.to_s)
def illegal_header_value?(header_value)
!!(ILLEGAL_HEADER_VALUE_REGEX =~ header_value.to_s)
end
private :possible_header_injection?
private :illegal_header_key?, :illegal_header_value?

# Fixup any headers with `,` in the name to have `_` now. We emit
# headers with `,` in them during the parse phase to avoid ambiguity
Expand Down Expand Up @@ -334,9 +341,11 @@ def req_env_post_parse(env)
def str_early_hints(headers)
eh_str = "HTTP/1.1 103 Early Hints\r\n".dup
headers.each_pair do |k, vs|
next if illegal_header_key?(k)

if vs.respond_to?(:to_s) && !vs.to_s.empty?
vs.to_s.split(NEWLINE).each do |v|
next if possible_header_injection?(v)
next if illegal_header_value?(v)
eh_str << "#{k}: #{v}\r\n"
end
else
Expand Down Expand Up @@ -399,9 +408,11 @@ def str_headers(env, status, headers, res_info, lines)
res_info[:response_hijack] = nil

headers.each do |k, vs|
next if illegal_header_key?(k)

case k.downcase
when CONTENT_LENGTH2
next if possible_header_injection?(vs)
next if illegal_header_value?(vs)
res_info[:content_length] = vs
next
when TRANSFER_ENCODING
Expand All @@ -410,11 +421,13 @@ def str_headers(env, status, headers, res_info, lines)
when HIJACK
res_info[:response_hijack] = vs
next
when BANNED_HEADER_KEY
next
end

if vs.respond_to?(:to_s) && !vs.to_s.empty?
vs.to_s.split(NEWLINE).each do |v|
next if possible_header_injection?(v)
next if illegal_header_value?(v)
lines.append k, colon, v, line_ending
end
else
Expand Down
11 changes: 0 additions & 11 deletions test/test_response_header.rb
Original file line number Diff line number Diff line change
Expand Up @@ -84,15 +84,11 @@ def assert_ignore_header(name, value, opts={})

# The header must not contain a Status key.
def test_status_key
skip 'implement later'

assert_ignore_header("Status", "500")
end

# Special headers starting “rack.” are for communicating with the server, and must not be sent back to the client.
def test_rack_key
skip 'implement later'

assert_ignore_header("rack.command_to_server_only", "work")
end

Expand All @@ -102,36 +98,29 @@ def test_rack_key
# Header keys will be set through two ways: Regular and early hints.

def test_illegal_character_in_key
skip 'implement later'
assert_ignore_header("\"F\u0000o\u0025(@o}", "Boo")
end

def test_illegal_character_in_key_when_early_hints
skip 'implement later'
assert_ignore_header("\"F\u0000o\u0025(@o}", "Boo", early_hints: true)
end

# testing header value can be separated by \n into line, and each line must not contain characters below 037
# Header values can be set through three ways: Regular, early hints and a special case for overriding content-length

def test_illegal_character_in_value
skip 'implement later'
assert_ignore_header("X-header", "First \000Lin\037e")
end

def test_illegal_character_in_value_when_early_hints
skip 'implement later'
assert_ignore_header("X-header", "First \000Lin\037e", early_hints: true)
end

def test_illegal_character_in_value_when_override_content_length
skip 'implement later'
assert_ignore_header("Content-Length", "\037")
end

def test_illegal_character_in_value_when_newline
skip 'implement later'

server_run app: ->(env) { [200, {'X-header' => "First\000 line\nSecond Lin\037e"}, ["Hello"]] }
data = send_http_and_read "GET / HTTP/1.0\r\n\r\n"

Expand Down