summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/webrick/httpauth/digestauth.rb8
-rw-r--r--lib/webrick/httprequest.rb23
-rw-r--r--lib/webrick/httpservlet/cgihandler.rb4
-rw-r--r--test/webrick/test_httpauth.rb90
-rw-r--r--test/webrick/test_httpserver.rb67
-rw-r--r--version.h2
6 files changed, 179 insertions, 15 deletions
diff --git a/lib/webrick/httpauth/digestauth.rb b/lib/webrick/httpauth/digestauth.rb
index 375ec20154..94f849a02e 100644
--- a/lib/webrick/httpauth/digestauth.rb
+++ b/lib/webrick/httpauth/digestauth.rb
@@ -235,9 +235,11 @@ module WEBrick
ha2 = hexdigest(req.request_method, auth_req['uri'])
ha2_res = hexdigest("", auth_req['uri'])
elsif auth_req['qop'] == "auth-int"
- ha2 = hexdigest(req.request_method, auth_req['uri'],
- hexdigest(req.body))
- ha2_res = hexdigest("", auth_req['uri'], hexdigest(res.body))
+ body_digest = @h.new
+ req.body { |chunk| body_digest.update(chunk) }
+ body_digest = body_digest.hexdigest
+ ha2 = hexdigest(req.request_method, auth_req['uri'], body_digest)
+ ha2_res = hexdigest("", auth_req['uri'], body_digest)
end
if auth_req['qop'] == "auth" || auth_req['qop'] == "auth-int"
diff --git a/lib/webrick/httprequest.rb b/lib/webrick/httprequest.rb
index 10cf72d288..b40bcb0d57 100644
--- a/lib/webrick/httprequest.rb
+++ b/lib/webrick/httprequest.rb
@@ -414,9 +414,13 @@ module WEBrick
MAX_URI_LENGTH = 2083 # :nodoc:
+ # same as Mongrel, Thin and Puma
+ MAX_HEADER_LENGTH = (112 * 1024) # :nodoc:
+
def read_request_line(socket)
@request_line = read_line(socket, MAX_URI_LENGTH) if socket
- if @request_line.bytesize >= MAX_URI_LENGTH and @request_line[-1, 1] != LF
+ @request_bytes = @request_line.bytesize
+ if @request_bytes >= MAX_URI_LENGTH and @request_line[-1, 1] != LF
raise HTTPStatus::RequestURITooLarge
end
@request_time = Time.now
@@ -435,6 +439,9 @@ module WEBrick
if socket
while line = read_line(socket)
break if /\A(#{CRLF}|#{LF})\z/om =~ line
+ if (@request_bytes += line.bytesize) > MAX_HEADER_LENGTH
+ raise HTTPStatus::RequestEntityTooLarge, 'headers too large'
+ end
@raw_header << line
end
end
@@ -502,12 +509,16 @@ module WEBrick
def read_chunked(socket, block)
chunk_size, = read_chunk_size(socket)
while chunk_size > 0
- data = read_data(socket, chunk_size) # read chunk-data
- if data.nil? || data.bytesize != chunk_size
- raise BadRequest, "bad chunk data size."
- end
+ begin
+ sz = [ chunk_size, @buffer_size ].min
+ data = read_data(socket, sz) # read chunk-data
+ if data.nil? || data.bytesize != sz
+ raise HTTPStatus::BadRequest, "bad chunk data size."
+ end
+ block.call(data)
+ end while (chunk_size -= sz) > 0
+
read_line(socket) # skip CRLF
- block.call(data)
chunk_size, = read_chunk_size(socket)
end
read_header(socket) # trailer + CRLF
diff --git a/lib/webrick/httpservlet/cgihandler.rb b/lib/webrick/httpservlet/cgihandler.rb
index ba6b0b6032..b1fb471c54 100644
--- a/lib/webrick/httpservlet/cgihandler.rb
+++ b/lib/webrick/httpservlet/cgihandler.rb
@@ -65,9 +65,7 @@ module WEBrick
cgi_in.write("%8d" % dump.bytesize)
cgi_in.write(dump)
- if req.body and req.body.bytesize > 0
- cgi_in.write(req.body)
- end
+ req.body { |chunk| cgi_in.write(chunk) }
ensure
cgi_in.close
status = $?.exitstatus
diff --git a/test/webrick/test_httpauth.rb b/test/webrick/test_httpauth.rb
index 2ee492db7a..ff539f06c7 100644
--- a/test/webrick/test_httpauth.rb
+++ b/test/webrick/test_httpauth.rb
@@ -4,6 +4,7 @@ require "net/http"
require "tempfile"
require "webrick"
require "webrick/httpauth/basicauth"
+require "stringio"
require_relative "utils"
class TestWEBrickHTTPAuth < Test::Unit::TestCase
@@ -216,12 +217,97 @@ class TestWEBrickHTTPAuth < Test::Unit::TestCase
}
end
+ def test_digest_auth_int
+ log_tester = lambda {|log, access_log|
+ log.reject! {|line| /\A\s*\z/ =~ line }
+ pats = [
+ /ERROR Digest wb auth-int realm: no credentials in the request\./,
+ /ERROR WEBrick::HTTPStatus::Unauthorized/,
+ /ERROR Digest wb auth-int realm: foo: digest unmatch\./
+ ]
+ pats.each {|pat|
+ assert(!log.grep(pat).empty?, "webrick log doesn't have expected error: #{pat.inspect}")
+ log.reject! {|line| pat =~ line }
+ }
+ assert_equal([], log)
+ }
+ TestWEBrick.start_httpserver({}, log_tester) {|server, addr, port, log|
+ realm = "wb auth-int realm"
+ path = "/digest_auth_int"
+
+ Tempfile.create("test_webrick_auth_int") {|tmpfile|
+ tmpfile.close
+ tmp_pass = WEBrick::HTTPAuth::Htdigest.new(tmpfile.path)
+ tmp_pass.set_passwd(realm, "foo", "Hunter2")
+ tmp_pass.flush
+
+ htdigest = WEBrick::HTTPAuth::Htdigest.new(tmpfile.path)
+ users = []
+ htdigest.each{|user, pass| users << user }
+ assert_equal %w(foo), users
+
+ auth = WEBrick::HTTPAuth::DigestAuth.new(
+ :Realm => realm, :UserDB => htdigest,
+ :Algorithm => 'MD5',
+ :Logger => server.logger,
+ :Qop => %w(auth-int),
+ )
+ server.mount_proc(path){|req, res|
+ auth.authenticate(req, res)
+ res.body = "bbb"
+ }
+ Net::HTTP.start(addr, port) do |http|
+ post = Net::HTTP::Post.new(path)
+ params = {}
+ data = 'hello=world'
+ body = StringIO.new(data)
+ post.content_length = data.bytesize
+ post['Content-Type'] = 'application/x-www-form-urlencoded'
+ post.body_stream = body
+
+ http.request(post) do |res|
+ assert_equal('401', res.code, log.call)
+ res["www-authenticate"].scan(DIGESTRES_) do |key, quoted, token|
+ params[key.downcase] = token || quoted.delete('\\')
+ end
+ params['uri'] = "http://#{addr}:#{port}#{path}"
+ end
+
+ body.rewind
+ cred = credentials_for_request('foo', 'Hunter3', params, body)
+ post['Authorization'] = cred
+ post.body_stream = body
+ http.request(post){|res|
+ assert_equal('401', res.code, log.call)
+ assert_not_equal("bbb", res.body, log.call)
+ }
+
+ body.rewind
+ cred = credentials_for_request('foo', 'Hunter2', params, body)
+ post['Authorization'] = cred
+ post.body_stream = body
+ http.request(post){|res| assert_equal("bbb", res.body, log.call)}
+ end
+ }
+ }
+ end
+
private
- def credentials_for_request(user, password, params)
+ def credentials_for_request(user, password, params, body = nil)
cnonce = "hoge"
nonce_count = 1
ha1 = "#{user}:#{params['realm']}:#{password}"
- ha2 = "GET:#{params['uri']}"
+ if body
+ dig = Digest::MD5.new
+ while buf = body.read(16384)
+ dig.update(buf)
+ end
+ body.rewind
+ ha2 = "POST:#{params['uri']}:#{dig.hexdigest}"
+ else
+ ha2 = "GET:#{params['uri']}"
+ end
+
request_digest =
"#{Digest::MD5.hexdigest(ha1)}:" \
"#{params['nonce']}:#{'%08x' % nonce_count}:#{cnonce}:#{params['qop']}:" \
diff --git a/test/webrick/test_httpserver.rb b/test/webrick/test_httpserver.rb
index daeb7b955e..024c0c510f 100644
--- a/test/webrick/test_httpserver.rb
+++ b/test/webrick/test_httpserver.rb
@@ -440,4 +440,71 @@ class TestWEBrickHTTPServer < Test::Unit::TestCase
s&.shutdown
th&.join
end
+
+ def test_gigantic_request_header
+ log_tester = lambda {|log, access_log|
+ assert_equal 1, log.size
+ assert log[0].include?('ERROR headers too large')
+ }
+ TestWEBrick.start_httpserver({}, log_tester){|server, addr, port, log|
+ server.mount('/', WEBrick::HTTPServlet::FileHandler, __FILE__)
+ TCPSocket.open(addr, port) do |c|
+ c.write("GET / HTTP/1.0\r\n")
+ junk = -"X-Junk: #{' ' * 1024}\r\n"
+ assert_raise(Errno::ECONNRESET, Errno::EPIPE) do
+ loop { c.write(junk) }
+ end
+ end
+ }
+ end
+
+ def test_eof_in_chunk
+ log_tester = lambda do |log, access_log|
+ assert_equal 1, log.size
+ assert log[0].include?('ERROR bad chunk data size')
+ end
+ TestWEBrick.start_httpserver({}, log_tester){|server, addr, port, log|
+ server.mount_proc('/', ->(req, res) { res.body = req.body })
+ TCPSocket.open(addr, port) do |c|
+ c.write("POST / HTTP/1.1\r\nHost: example.com\r\n" \
+ "Transfer-Encoding: chunked\r\n\r\n5\r\na")
+ c.shutdown(Socket::SHUT_WR) # trigger EOF in server
+ res = c.read
+ assert_match %r{\AHTTP/1\.1 400 }, res
+ end
+ }
+ end
+
+ def test_big_chunks
+ nr_out = 3
+ buf = 'big' # 3 bytes is bigger than 2!
+ config = { :InputBufferSize => 2 }.freeze
+ total = 0
+ all = ''
+ TestWEBrick.start_httpserver(config){|server, addr, port, log|
+ server.mount_proc('/', ->(req, res) {
+ err = []
+ ret = req.body do |chunk|
+ n = chunk.bytesize
+ n > config[:InputBufferSize] and err << "#{n} > :InputBufferSize"
+ total += n
+ all << chunk
+ end
+ ret.nil? or err << 'req.body should return nil'
+ (buf * nr_out) == all or err << 'input body does not match expected'
+ res.header['connection'] = 'close'
+ res.body = err.join("\n")
+ })
+ TCPSocket.open(addr, port) do |c|
+ c.write("POST / HTTP/1.1\r\nHost: example.com\r\n" \
+ "Transfer-Encoding: chunked\r\n\r\n")
+ chunk = "#{buf.bytesize.to_s(16)}\r\n#{buf}\r\n"
+ nr_out.times { c.write(chunk) }
+ c.write("0\r\n\r\n")
+ head, body = c.read.split("\r\n\r\n")
+ assert_match %r{\AHTTP/1\.1 200 OK}, head
+ assert_nil body
+ end
+ }
+ end
end
diff --git a/version.h b/version.h
index bbc812d5c6..d18aa6173b 100644
--- a/version.h
+++ b/version.h
@@ -1,6 +1,6 @@
#define RUBY_VERSION "2.4.4"
#define RUBY_RELEASE_DATE "2018-03-28"
-#define RUBY_PATCHLEVEL 294
+#define RUBY_PATCHLEVEL 295
#define RUBY_RELEASE_YEAR 2018
#define RUBY_RELEASE_MONTH 3