High severity8.8NVD Advisory· Published Sep 19, 2017· Updated May 13, 2026
CVE-2017-10784
CVE-2017-10784
Description
The Basic authentication code in WEBrick library in Ruby before 2.2.8, 2.3.x before 2.3.5, and 2.4.x through 2.4.1 allows remote attackers to inject terminal emulator escape sequences into its log and possibly execute arbitrary commands via a crafted user name.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
webrickRubyGems | < 1.4.0 | 1.4.0 |
Patches
24ac0f3843ab8Picked commits from ruby core repository.
13 files changed · +182 −44
lib/webrick/httpproxy.rb+4 −4 modified@@ -193,13 +193,13 @@ def do_CONNECT(req, res) begin while fds = IO::select([ua, os]) if fds[0].member?(ua) - buf = ua.sysread(1024); + buf = ua.readpartial(1024); @logger.debug("CONNECT: #{buf.bytesize} byte from User-Agent") - os.syswrite(buf) + os.write(buf) elsif fds[0].member?(os) - buf = os.sysread(1024); + buf = os.readpartial(1024); @logger.debug("CONNECT: #{buf.bytesize} byte from #{host}:#{port}") - ua.syswrite(buf) + ua.write(buf) end end rescue
lib/webrick/httpresponse.rb+50 −16 modified@@ -303,6 +303,8 @@ def send_header(socket) # :nodoc: def send_body(socket) # :nodoc: if @body.respond_to? :readpartial then send_body_io(socket) + elsif @body.respond_to?(:call) then + send_body_proc(socket) else send_body_string(socket) end @@ -394,19 +396,18 @@ def send_body_io(socket) if @request_method == "HEAD" # do nothing elsif chunked? + buf = '' begin - buf = '' - data = '' - while true - @body.readpartial( @buffer_size, buf ) # there is no need to clear buf? - data << format("%x", buf.bytesize) << CRLF - data << buf << CRLF - _write_data(socket, data) - data.clear - @sent_size += buf.bytesize - end - rescue EOFError # do nothing - end + @body.readpartial(@buffer_size, buf) + size = buf.bytesize + data = "#{size.to_s(16)}#{CRLF}#{buf}#{CRLF}" + _write_data(socket, data) + data.clear + @sent_size += size + rescue EOFError + break + end while true + buf.clear _write_data(socket, "0#{CRLF}#{CRLF}") else size = @header['content-length'].to_i @@ -425,11 +426,11 @@ def send_body_string(socket) body ? @body.bytesize : 0 while buf = @body[@sent_size, @buffer_size] break if buf.empty? - data = "" - data << format("%x", buf.bytesize) << CRLF - data << buf << CRLF + size = buf.bytesize + data = "#{size.to_s(16)}#{CRLF}#{buf}#{CRLF}" + buf.clear _write_data(socket, data) - @sent_size += buf.bytesize + @sent_size += size end _write_data(socket, "0#{CRLF}#{CRLF}") else @@ -440,6 +441,39 @@ def send_body_string(socket) end end + def send_body_proc(socket) + if @request_method == "HEAD" + # do nothing + elsif chunked? + @body.call(ChunkedWrapper.new(socket, self)) + _write_data(socket, "0#{CRLF}#{CRLF}") + else + size = @header['content-length'].to_i + @body.call(socket) + @sent_size = size + end + end + + class ChunkedWrapper + def initialize(socket, resp) + @socket = socket + @resp = resp + end + + def write(buf) + return if buf.empty? + socket = @socket + @resp.instance_eval { + size = buf.bytesize + data = "#{size.to_s(16)}#{CRLF}#{buf}#{CRLF}" + _write_data(socket, data) + data.clear + @sent_size += size + } + end + alias :<< :write + end + def _send_file(output, input, offset, size) while offset > 0 sz = @buffer_size < size ? @buffer_size : size
lib/webrick/httpservlet/abstract.rb+0 −2 modified@@ -9,8 +9,6 @@ # # $IPR: abstract.rb,v 1.24 2003/07/11 11:16:46 gotoyuzo Exp $ -require 'thread' - require 'webrick/htmlutils' require 'webrick/httputils' require 'webrick/httpstatus'
lib/webrick/httpservlet/filehandler.rb+0 −1 modified@@ -9,7 +9,6 @@ # # $IPR: filehandler.rb,v 1.44 2003/06/07 01:34:51 gotoyuzo Exp $ -require 'thread' require 'time' require 'webrick/htmlutils'
lib/webrick/httpstatus.rb+0 −4 modified@@ -23,10 +23,6 @@ module HTTPStatus ## # Root of the HTTP status class hierarchy class Status < StandardError - def initialize(*args) # :nodoc: - args[0] = AccessLog.escape(args[0]) unless args.empty? - super(*args) - end class << self attr_reader :code, :reason_phrase # :nodoc: end
lib/webrick/httputils.rb+1 −0 modified@@ -69,6 +69,7 @@ def normalize_path(path) "jpeg" => "image/jpeg", "jpg" => "image/jpeg", "js" => "application/javascript", + "json" => "application/json", "lha" => "application/octet-stream", "lzh" => "application/octet-stream", "mov" => "video/quicktime",
lib/webrick/log.rb+2 −2 modified@@ -118,10 +118,10 @@ def debug?; @level >= DEBUG; end # * Otherwise it will return +arg+.inspect. def format(arg) if arg.is_a?(Exception) - "#{arg.class}: #{arg.message}\n\t" << + "#{arg.class}: #{AccessLog.escape(arg.message)}\n\t" << arg.backtrace.join("\n\t") << "\n" elsif arg.respond_to?(:to_str) - arg.to_str + AccessLog.escape(arg.to_str) else arg.inspect end
lib/webrick/server.rb+30 −13 modified@@ -9,7 +9,6 @@ # # $IPR: server.rb,v 1.62 2003/07/22 19:20:43 gotoyuzo Exp $ -require 'thread' require 'socket' require 'webrick/config' require 'webrick/log' @@ -168,7 +167,7 @@ def start(&block) while @status == :Running begin sp = shutdown_pipe[0] - if svrs = IO.select([sp, *@listeners], nil, nil, 2.0) + if svrs = IO.select([sp, *@listeners]) if svrs[0].include? sp # swallow shutdown pipe buf = String.new @@ -252,18 +251,26 @@ def run(sock) # the client socket. def accept_client(svr) - sock = nil - begin - sock = svr.accept - sock.sync = true - Utils::set_non_blocking(sock) - rescue Errno::ECONNRESET, Errno::ECONNABORTED, - Errno::EPROTO, Errno::EINVAL - rescue StandardError => ex - msg = "#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}" - @logger.error msg + case sock = svr.to_io.accept_nonblock(exception: false) + when :wait_readable + nil + else + if svr.respond_to?(:start_immediately) + sock = OpenSSL::SSL::SSLSocket.new(sock, ssl_context) + sock.sync_close = true + # we cannot do OpenSSL::SSL::SSLSocket#accept here because + # a slow client can prevent us from accepting connections + # from other clients + end + sock end - return sock + rescue Errno::ECONNRESET, Errno::ECONNABORTED, + Errno::EPROTO, Errno::EINVAL + nil + rescue StandardError => ex + msg = "#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}" + @logger.error msg + nil end ## @@ -286,6 +293,16 @@ def start_thread(sock, &block) @logger.debug "accept: <address unknown>" raise end + if sock.respond_to?(:sync_close=) && @config[:SSLStartImmediately] + WEBrick::Utils.timeout(@config[:RequestTimeout]) do + begin + sock.accept # OpenSSL::SSL::SSLSocket#accept + rescue Errno::ECONNRESET, Errno::ECONNABORTED, + Errno::EPROTO, Errno::EINVAL + Thread.exit + end + end + end call_callback(:AcceptCallback, sock) block ? block.call(sock) : run(sock) rescue Errno::ENOTCONN
lib/webrick/utils.rb+0 −1 modified@@ -91,7 +91,6 @@ def random_string(len) ########### - require "thread" require "timeout" require "singleton"
test/webrick/test_httpauth.rb+36 −0 modified@@ -103,6 +103,42 @@ def test_basic_auth3 } end + def test_bad_username_with_control_characters + log_tester = lambda {|log, access_log| + assert_equal(2, log.length) + assert_match(/ERROR Basic WEBrick's realm: foo\\ebar: the user is not allowed./, log[0]) + assert_match(/ERROR WEBrick::HTTPStatus::Unauthorized/, log[1]) + } + TestWEBrick.start_httpserver({}, log_tester) {|server, addr, port, log| + realm = "WEBrick's realm" + path = "/basic_auth" + + Tempfile.create("test_webrick_auth") {|tmpfile| + tmpfile.close + tmp_pass = WEBrick::HTTPAuth::Htpasswd.new(tmpfile.path) + tmp_pass.set_passwd(realm, "webrick", "supersecretpassword") + tmp_pass.set_passwd(realm, "foo", "supersecretpassword") + tmp_pass.flush + + htpasswd = WEBrick::HTTPAuth::Htpasswd.new(tmpfile.path) + users = [] + htpasswd.each{|user, pass| users << user } + server.mount_proc(path){|req, res| + auth = WEBrick::HTTPAuth::BasicAuth.new( + :Realm => realm, :UserDB => htpasswd, + :Logger => server.logger + ) + auth.authenticate(req, res) + res.body = "hoge" + } + http = Net::HTTP.new(addr, port) + g = Net::HTTP::Get.new(path) + g.basic_auth("foo\ebar", "passwd") + http.request(g){|res| assert_not_equal("hoge", res.body, log.call) } + } + } + end + DIGESTRES_ = / ([a-zA-Z\-]+) [ \t]*(?:\r\n[ \t]*)*
test/webrick/test_httpresponse.rb+23 −0 modified@@ -147,6 +147,29 @@ def test_send_body_string_io_chunked assert_equal 0, logger.messages.length end + def test_send_body_proc + @res.body = Proc.new { |out| out.write('hello') } + IO.pipe do |r, w| + @res.send_body(w) + w.close + r.binmode + assert_equal 'hello', r.read + end + assert_equal 0, logger.messages.length + end + + def test_send_body_proc_chunked + @res.body = Proc.new { |out| out.write('hello') } + @res.chunked = true + IO.pipe do |r, w| + @res.send_body(w) + w.close + r.binmode + assert_equal "5\r\nhello\r\n0\r\n\r\n", r.read + end + assert_equal 0, logger.messages.length + end + def test_set_error status = 400 message = 'missing attribute'
test/webrick/test_ssl_server.rb+27 −0 modified@@ -2,6 +2,7 @@ require "webrick" require "webrick/ssl" require_relative "utils" +require 'timeout' class TestWEBrickSSLServer < Test::Unit::TestCase class Echo < WEBrick::GenericServer @@ -37,4 +38,30 @@ def assert_self_signed_cert(config) io.close } end + + def test_slow_connect + poke = lambda do |io, msg| + begin + sock = OpenSSL::SSL::SSLSocket.new(io) + sock.connect + sock.puts(msg) + assert_equal "#{msg}\n", sock.gets, msg + ensure + sock&.close + io.close + end + end + config = { + :SSLEnable => true, + :SSLCertName => "/C=JP/O=www.ruby-lang.org/CN=Ruby", + } + Timeout.timeout(10) do + TestWEBrick.start_server(Echo, config) do |server, addr, port, log| + outer = TCPSocket.new(addr, port) + inner = TCPSocket.new(addr, port) + poke.call(inner, 'fast TLS negotiation') + poke.call(outer, 'slow TLS negotiation') + end + end + end end
webrick.gemspec+9 −1 modified@@ -15,8 +15,16 @@ Gem::Specification.new do |s| s.authors = ["TAKAHASHI Masayoshi", "GOTOU YUUZOU"] s.email = [nil, nil] - s.homepage = "https://github.com/ruby/webrick" + s.homepage = "https://www.ruby-lang.org" s.license = "BSD-2-Clause" + if s.respond_to?(:metadata=) + s.metadata = { + "bug_tracker_uri" => "https://bugs.ruby-lang.org/projects/ruby-trunk/issues", + "homepage_uri" => "https://www.ruby-lang.org", + "source_code_uri" => "https://svn.ruby-lang.org/repos/ruby" + } + end + s.add_development_dependency "rake" end
6617c41292lib/webrick/log.rb: sanitize any type of logs
3 files changed · +38 −6
lib/webrick/httpstatus.rb+0 −4 modified@@ -23,10 +23,6 @@ module HTTPStatus ## # Root of the HTTP status class hierarchy class Status < StandardError - def initialize(*args) # :nodoc: - args[0] = AccessLog.escape(args[0]) unless args.empty? - super(*args) - end class << self attr_reader :code, :reason_phrase # :nodoc: end
lib/webrick/log.rb+2 −2 modified@@ -118,10 +118,10 @@ def debug?; @level >= DEBUG; end # * Otherwise it will return +arg+.inspect. def format(arg) if arg.is_a?(Exception) - "#{arg.class}: #{arg.message}\n\t" << + "#{arg.class}: #{AccessLog.escape(arg.message)}\n\t" << arg.backtrace.join("\n\t") << "\n" elsif arg.respond_to?(:to_str) - arg.to_str + AccessLog.escape(arg.to_str) else arg.inspect end
test/webrick/test_httpauth.rb+36 −0 modified@@ -103,6 +103,42 @@ def test_basic_auth3 } end + def test_bad_username_with_control_characters + log_tester = lambda {|log, access_log| + assert_equal(2, log.length) + assert_match(/ERROR Basic WEBrick's realm: foo\\ebar: the user is not allowed./, log[0]) + assert_match(/ERROR WEBrick::HTTPStatus::Unauthorized/, log[1]) + } + TestWEBrick.start_httpserver({}, log_tester) {|server, addr, port, log| + realm = "WEBrick's realm" + path = "/basic_auth" + + Tempfile.create("test_webrick_auth") {|tmpfile| + tmpfile.close + tmp_pass = WEBrick::HTTPAuth::Htpasswd.new(tmpfile.path) + tmp_pass.set_passwd(realm, "webrick", "supersecretpassword") + tmp_pass.set_passwd(realm, "foo", "supersecretpassword") + tmp_pass.flush + + htpasswd = WEBrick::HTTPAuth::Htpasswd.new(tmpfile.path) + users = [] + htpasswd.each{|user, pass| users << user } + server.mount_proc(path){|req, res| + auth = WEBrick::HTTPAuth::BasicAuth.new( + :Realm => realm, :UserDB => htpasswd, + :Logger => server.logger + ) + auth.authenticate(req, res) + res.body = "hoge" + } + http = Net::HTTP.new(addr, port) + g = Net::HTTP::Get.new(path) + g.basic_auth("foo\ebar", "passwd") + http.request(g){|res| assert_not_equal("hoge", res.body, log.call) } + } + } + end + DIGESTRES_ = / ([a-zA-Z\-]+) [ \t]*(?:\r\n[ \t]*)*
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
29- www.ruby-lang.org/en/news/2017/09/14/ruby-2-2-8-released/nvdPatchVendor Advisory
- www.ruby-lang.org/en/news/2017/09/14/ruby-2-3-5-released/nvdPatchVendor Advisory
- www.securityfocus.com/bid/100853nvdThird Party AdvisoryVDB Entry
- www.securitytracker.com/id/1039363nvdThird Party AdvisoryVDB Entry
- github.com/advisories/GHSA-369m-2gv6-mw28ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2017-10784ghsaADVISORY
- www.ruby-lang.org/en/news/2017/09/14/webrick-basic-auth-escape-sequence-injection-cve-2017-10784/nvdVendor Advisory
- access.redhat.com/errata/RHSA-2017:3485nvdWEB
- access.redhat.com/errata/RHSA-2018:0378nvdWEB
- access.redhat.com/errata/RHSA-2018:0583nvdWEB
- access.redhat.com/errata/RHSA-2018:0585nvdWEB
- github.com/ruby/ruby/commit/6617c41292ghsaWEB
- github.com/ruby/webrick/commit/4ac0f3843ab82d1c31e1cfc719409208adef7813ghsaWEB
- github.com/rubysec/ruby-advisory-db/blob/master/gems/webrick/CVE-2017-10784.ymlghsaWEB
- hackerone.com/reports/223363ghsaWEB
- lists.debian.org/debian-lts-announce/2018/07/msg00012.htmlnvdWEB
- security.gentoo.org/glsa/201710-18nvdWEB
- usn.ubuntu.com/3528-1ghsaWEB
- usn.ubuntu.com/3685-1ghsaWEB
- web.archive.org/web/20210621131814/http://www.securityfocus.com/bid/100853ghsaWEB
- web.archive.org/web/20210919031115/http://www.securitytracker.com/id/1042004ghsaWEB
- web.archive.org/web/20211025092552/http://www.securitytracker.com/id/1039363ghsaWEB
- www.debian.org/security/2017/dsa-4031nvdWEB
- www.ruby-lang.org/en/news/2017/09/14/ruby-2-2-8-releasedghsaWEB
- www.ruby-lang.org/en/news/2017/09/14/ruby-2-3-5-releasedghsaWEB
- www.ruby-lang.org/en/news/2017/09/14/webrick-basic-auth-escape-sequence-injection-cve-2017-10784ghsaWEB
- www.securitytracker.com/id/1042004nvd
- usn.ubuntu.com/3528-1/nvd
- usn.ubuntu.com/3685-1/nvd
News mentions
0No linked articles in our index yet.