[ruby/cgi] Prevent CRLF injection

Throw a RuntimeError if the HTTP response header contains CR or LF to
prevent HTTP response splitting.

https://hackerone.com/reports/1204695

https://github.com/ruby/cgi/commit/64c5045c0a
This commit is contained in:
Yusuke Endoh 2022-11-22 10:49:27 +09:00 committed by git
parent c05f85f373
commit 0e75b2f2e6
2 changed files with 36 additions and 17 deletions

View File

@ -188,17 +188,28 @@ class CGI
# Using #header with the HTML5 tag maker will create a <header> element. # Using #header with the HTML5 tag maker will create a <header> element.
alias :header :http_header alias :header :http_header
def _no_crlf_check(str)
if str
str = str.to_s
raise "A HTTP status or header field must not include CR and LF" if str =~ /[\r\n]/
str
else
nil
end
end
private :_no_crlf_check
def _header_for_string(content_type) #:nodoc: def _header_for_string(content_type) #:nodoc:
buf = ''.dup buf = ''.dup
if nph?() if nph?()
buf << "#{$CGI_ENV['SERVER_PROTOCOL'] || 'HTTP/1.0'} 200 OK#{EOL}" buf << "#{_no_crlf_check($CGI_ENV['SERVER_PROTOCOL']) || 'HTTP/1.0'} 200 OK#{EOL}"
buf << "Date: #{CGI.rfc1123_date(Time.now)}#{EOL}" buf << "Date: #{CGI.rfc1123_date(Time.now)}#{EOL}"
buf << "Server: #{$CGI_ENV['SERVER_SOFTWARE']}#{EOL}" buf << "Server: #{_no_crlf_check($CGI_ENV['SERVER_SOFTWARE'])}#{EOL}"
buf << "Connection: close#{EOL}" buf << "Connection: close#{EOL}"
end end
buf << "Content-Type: #{content_type}#{EOL}" buf << "Content-Type: #{_no_crlf_check(content_type)}#{EOL}"
if @output_cookies if @output_cookies
@output_cookies.each {|cookie| buf << "Set-Cookie: #{cookie}#{EOL}" } @output_cookies.each {|cookie| buf << "Set-Cookie: #{_no_crlf_check(cookie)}#{EOL}" }
end end
return buf return buf
end # _header_for_string end # _header_for_string
@ -213,9 +224,9 @@ class CGI
## NPH ## NPH
options.delete('nph') if defined?(MOD_RUBY) options.delete('nph') if defined?(MOD_RUBY)
if options.delete('nph') || nph?() if options.delete('nph') || nph?()
protocol = $CGI_ENV['SERVER_PROTOCOL'] || 'HTTP/1.0' protocol = _no_crlf_check($CGI_ENV['SERVER_PROTOCOL']) || 'HTTP/1.0'
status = options.delete('status') status = options.delete('status')
status = HTTP_STATUS[status] || status || '200 OK' status = HTTP_STATUS[status] || _no_crlf_check(status) || '200 OK'
buf << "#{protocol} #{status}#{EOL}" buf << "#{protocol} #{status}#{EOL}"
buf << "Date: #{CGI.rfc1123_date(Time.now)}#{EOL}" buf << "Date: #{CGI.rfc1123_date(Time.now)}#{EOL}"
options['server'] ||= $CGI_ENV['SERVER_SOFTWARE'] || '' options['server'] ||= $CGI_ENV['SERVER_SOFTWARE'] || ''
@ -223,38 +234,38 @@ class CGI
end end
## common headers ## common headers
status = options.delete('status') status = options.delete('status')
buf << "Status: #{HTTP_STATUS[status] || status}#{EOL}" if status buf << "Status: #{HTTP_STATUS[status] || _no_crlf_check(status)}#{EOL}" if status
server = options.delete('server') server = options.delete('server')
buf << "Server: #{server}#{EOL}" if server buf << "Server: #{_no_crlf_check(server)}#{EOL}" if server
connection = options.delete('connection') connection = options.delete('connection')
buf << "Connection: #{connection}#{EOL}" if connection buf << "Connection: #{_no_crlf_check(connection)}#{EOL}" if connection
type = options.delete('type') type = options.delete('type')
buf << "Content-Type: #{type}#{EOL}" #if type buf << "Content-Type: #{_no_crlf_check(type)}#{EOL}" #if type
length = options.delete('length') length = options.delete('length')
buf << "Content-Length: #{length}#{EOL}" if length buf << "Content-Length: #{_no_crlf_check(length)}#{EOL}" if length
language = options.delete('language') language = options.delete('language')
buf << "Content-Language: #{language}#{EOL}" if language buf << "Content-Language: #{_no_crlf_check(language)}#{EOL}" if language
expires = options.delete('expires') expires = options.delete('expires')
buf << "Expires: #{CGI.rfc1123_date(expires)}#{EOL}" if expires buf << "Expires: #{CGI.rfc1123_date(expires)}#{EOL}" if expires
## cookie ## cookie
if cookie = options.delete('cookie') if cookie = options.delete('cookie')
case cookie case cookie
when String, Cookie when String, Cookie
buf << "Set-Cookie: #{cookie}#{EOL}" buf << "Set-Cookie: #{_no_crlf_check(cookie)}#{EOL}"
when Array when Array
arr = cookie arr = cookie
arr.each {|c| buf << "Set-Cookie: #{c}#{EOL}" } arr.each {|c| buf << "Set-Cookie: #{_no_crlf_check(c)}#{EOL}" }
when Hash when Hash
hash = cookie hash = cookie
hash.each_value {|c| buf << "Set-Cookie: #{c}#{EOL}" } hash.each_value {|c| buf << "Set-Cookie: #{_no_crlf_check(c)}#{EOL}" }
end end
end end
if @output_cookies if @output_cookies
@output_cookies.each {|c| buf << "Set-Cookie: #{c}#{EOL}" } @output_cookies.each {|c| buf << "Set-Cookie: #{_no_crlf_check(c)}#{EOL}" }
end end
## other headers ## other headers
options.each do |key, value| options.each do |key, value|
buf << "#{key}: #{value}#{EOL}" buf << "#{_no_crlf_check(key)}: #{_no_crlf_check(value)}#{EOL}"
end end
return buf return buf
end # _header_for_hash end # _header_for_hash

View File

@ -176,6 +176,14 @@ class CGIHeaderTest < Test::Unit::TestCase
end end
def test_cgi_http_header_crlf_injection
cgi = CGI.new
assert_raise(RuntimeError) { cgi.http_header("text/xhtml\r\nBOO") }
assert_raise(RuntimeError) { cgi.http_header("type" => "text/xhtml\r\nBOO") }
assert_raise(RuntimeError) { cgi.http_header("status" => "200 OK\r\nBOO") }
assert_raise(RuntimeError) { cgi.http_header("location" => "text/xhtml\r\nBOO") }
end
instance_methods.each do |method| instance_methods.each do |method|
private method if method =~ /^test_(.*)/ && $1 != ENV['TEST'] private method if method =~ /^test_(.*)/ && $1 != ENV['TEST']