VYPR
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.

PackageAffected versionsPatched versions
webrickRubyGems
< 1.4.01.4.0

Patches

2
4ac0f3843ab8

Picked commits from ruby core repository.

https://github.com/ruby/webrickSHIBATA HiroshiNov 28, 2017via ghsa
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
    
6617c41292

lib/webrick/log.rb: sanitize any type of logs

https://github.com/ruby/rubymameSep 14, 2017via ghsa
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

News mentions

0

No linked articles in our index yet.