mirror of
https://github.com/ruby/ruby.git
synced 2022-11-09 12:17:21 -05:00

webrick/httprequest: limit request headers size We use the same 112 KB limit started (AFAIK) by Mongrel, Thin, and Puma to prevent malicious users from using up all the memory with a single request. This also limits the damage done by excessive ranges in multipart Range: requests. Due to the way we rely on IO#gets and the desire to keep the code simple, the actual maximum header may be 4093 bytes larger than 112 KB, but we're splitting hairs at that point. * lib/webrick/httprequest.rb: define MAX_HEADER_LENGTH (read_header): raise when headers exceed max length webrick/httpservlet/cgihandler: reduce memory use WEBrick::HTTPRequest#body can be passed a block to process the body in chunks. Use this feature to avoid building a giant string in memory. * lib/webrick/httpservlet/cgihandler.rb (do_GET): avoid reading entire request body into memory (do_POST is aliased to do_GET, so it handles bodies) webrick/httprequest: raise correct exception "BadRequest" alone does not resolve correctly, it is in the HTTPStatus namespace. * lib/webrick/httprequest.rb (read_chunked): use correct exception * test/webrick/test_httpserver.rb (test_eof_in_chunk): new test webrick/httprequest: use InputBufferSize for chunked requests While WEBrick::HTTPRequest#body provides a Proc interface for streaming large request bodies, clients must not force the server to use an excessively large chunk size. * lib/webrick/httprequest.rb (read_chunk_size): limit each read and block.call to :InputBufferSize in config. * test/webrick/test_httpserver.rb (test_big_chunks): new test webrick: add test for Digest auth-int No changes to the actual code, this is a new test for a feature for which no tests existed. I don't understand the Digest authentication code well at all, but this is necessary for the subsequent change. * test/webrick/test_httpauth.rb (test_digest_auth_int): new test (credentials_for_request): support bodies with POST webrick/httpauth/digestauth: stream req.body WARNING! WARNING! WARNING! LIKELY BROKEN CHANGE Pass a proc to WEBrick::HTTPRequest#body to avoid reading a potentially large request body into memory during authentication. WARNING! this will break apps completely which want to do something with the body besides calculating the MD5 digest of it. Also, keep in mind that probably nobody uses "auth-int". Servers such as Apache, lighttpd, nginx don't seem to support it; nor does curl when using POST/PUT bodies; and we didn't have tests for it until now... * lib/webrick/httpauth/digestauth.rb (_authenticate): stream req.body git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/branches/ruby_2_4@63004 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
410 lines
13 KiB
Ruby
410 lines
13 KiB
Ruby
# frozen_string_literal: false
|
|
#
|
|
# httpauth/digestauth.rb -- HTTP digest access authentication
|
|
#
|
|
# Author: IPR -- Internet Programming with Ruby -- writers
|
|
# Copyright (c) 2003 Internet Programming with Ruby writers.
|
|
# Copyright (c) 2003 H.M.
|
|
#
|
|
# The original implementation is provided by H.M.
|
|
# URL: http://rwiki.jin.gr.jp/cgi-bin/rw-cgi.rb?cmd=view;name=
|
|
# %C7%A7%BE%DA%B5%A1%C7%BD%A4%F2%B2%FE%C2%A4%A4%B7%A4%C6%A4%DF%A4%EB
|
|
#
|
|
# $IPR: digestauth.rb,v 1.5 2003/02/20 07:15:47 gotoyuzo Exp $
|
|
|
|
require 'webrick/config'
|
|
require 'webrick/httpstatus'
|
|
require 'webrick/httpauth/authenticator'
|
|
require 'digest/md5'
|
|
require 'digest/sha1'
|
|
|
|
module WEBrick
|
|
module HTTPAuth
|
|
|
|
##
|
|
# RFC 2617 Digest Access Authentication for WEBrick
|
|
#
|
|
# Use this class to add digest authentication to a WEBrick servlet.
|
|
#
|
|
# Here is an example of how to set up DigestAuth:
|
|
#
|
|
# config = { :Realm => 'DigestAuth example realm' }
|
|
#
|
|
# htdigest = WEBrick::HTTPAuth::Htdigest.new 'my_password_file'
|
|
# htdigest.set_passwd config[:Realm], 'username', 'password'
|
|
# htdigest.flush
|
|
#
|
|
# config[:UserDB] = htdigest
|
|
#
|
|
# digest_auth = WEBrick::HTTPAuth::DigestAuth.new config
|
|
#
|
|
# When using this as with a servlet be sure not to create a new DigestAuth
|
|
# object in the servlet's #initialize. By default WEBrick creates a new
|
|
# servlet instance for every request and the DigestAuth object must be
|
|
# used across requests.
|
|
|
|
class DigestAuth
|
|
include Authenticator
|
|
|
|
AuthScheme = "Digest" # :nodoc:
|
|
|
|
##
|
|
# Struct containing the opaque portion of the digest authentication
|
|
|
|
OpaqueInfo = Struct.new(:time, :nonce, :nc) # :nodoc:
|
|
|
|
##
|
|
# Digest authentication algorithm
|
|
|
|
attr_reader :algorithm
|
|
|
|
##
|
|
# Quality of protection. RFC 2617 defines "auth" and "auth-int"
|
|
|
|
attr_reader :qop
|
|
|
|
##
|
|
# Used by UserDB to create a digest password entry
|
|
|
|
def self.make_passwd(realm, user, pass)
|
|
pass ||= ""
|
|
Digest::MD5::hexdigest([user, realm, pass].join(":"))
|
|
end
|
|
|
|
##
|
|
# Creates a new DigestAuth instance. Be sure to use the same DigestAuth
|
|
# instance for multiple requests as it saves state between requests in
|
|
# order to perform authentication.
|
|
#
|
|
# See WEBrick::Config::DigestAuth for default configuration entries
|
|
#
|
|
# You must supply the following configuration entries:
|
|
#
|
|
# :Realm:: The name of the realm being protected.
|
|
# :UserDB:: A database of usernames and passwords.
|
|
# A WEBrick::HTTPAuth::Htdigest instance should be used.
|
|
|
|
def initialize(config, default=Config::DigestAuth)
|
|
check_init(config)
|
|
@config = default.dup.update(config)
|
|
@algorithm = @config[:Algorithm]
|
|
@domain = @config[:Domain]
|
|
@qop = @config[:Qop]
|
|
@use_opaque = @config[:UseOpaque]
|
|
@use_next_nonce = @config[:UseNextNonce]
|
|
@check_nc = @config[:CheckNc]
|
|
@use_auth_info_header = @config[:UseAuthenticationInfoHeader]
|
|
@nonce_expire_period = @config[:NonceExpirePeriod]
|
|
@nonce_expire_delta = @config[:NonceExpireDelta]
|
|
@internet_explorer_hack = @config[:InternetExplorerHack]
|
|
|
|
case @algorithm
|
|
when 'MD5','MD5-sess'
|
|
@h = Digest::MD5
|
|
when 'SHA1','SHA1-sess' # it is a bonus feature :-)
|
|
@h = Digest::SHA1
|
|
else
|
|
msg = format('Algorithm "%s" is not supported.', @algorithm)
|
|
raise ArgumentError.new(msg)
|
|
end
|
|
|
|
@instance_key = hexdigest(self.__id__, Time.now.to_i, Process.pid)
|
|
@opaques = {}
|
|
@last_nonce_expire = Time.now
|
|
@mutex = Thread::Mutex.new
|
|
end
|
|
|
|
##
|
|
# Authenticates a +req+ and returns a 401 Unauthorized using +res+ if
|
|
# the authentication was not correct.
|
|
|
|
def authenticate(req, res)
|
|
unless result = @mutex.synchronize{ _authenticate(req, res) }
|
|
challenge(req, res)
|
|
end
|
|
if result == :nonce_is_stale
|
|
challenge(req, res, true)
|
|
end
|
|
return true
|
|
end
|
|
|
|
##
|
|
# Returns a challenge response which asks for authentication information
|
|
|
|
def challenge(req, res, stale=false)
|
|
nonce = generate_next_nonce(req)
|
|
if @use_opaque
|
|
opaque = generate_opaque(req)
|
|
@opaques[opaque].nonce = nonce
|
|
end
|
|
|
|
param = Hash.new
|
|
param["realm"] = HTTPUtils::quote(@realm)
|
|
param["domain"] = HTTPUtils::quote(@domain.to_a.join(" ")) if @domain
|
|
param["nonce"] = HTTPUtils::quote(nonce)
|
|
param["opaque"] = HTTPUtils::quote(opaque) if opaque
|
|
param["stale"] = stale.to_s
|
|
param["algorithm"] = @algorithm
|
|
param["qop"] = HTTPUtils::quote(@qop.to_a.join(",")) if @qop
|
|
|
|
res[@response_field] =
|
|
"#{@auth_scheme} " + param.map{|k,v| "#{k}=#{v}" }.join(", ")
|
|
info("%s: %s", @response_field, res[@response_field]) if $DEBUG
|
|
raise @auth_exception
|
|
end
|
|
|
|
private
|
|
|
|
# :stopdoc:
|
|
|
|
MustParams = ['username','realm','nonce','uri','response']
|
|
MustParamsAuth = ['cnonce','nc']
|
|
|
|
def _authenticate(req, res)
|
|
unless digest_credentials = check_scheme(req)
|
|
return false
|
|
end
|
|
|
|
auth_req = split_param_value(digest_credentials)
|
|
if auth_req['qop'] == "auth" || auth_req['qop'] == "auth-int"
|
|
req_params = MustParams + MustParamsAuth
|
|
else
|
|
req_params = MustParams
|
|
end
|
|
req_params.each{|key|
|
|
unless auth_req.has_key?(key)
|
|
error('%s: parameter missing. "%s"', auth_req['username'], key)
|
|
raise HTTPStatus::BadRequest
|
|
end
|
|
}
|
|
|
|
if !check_uri(req, auth_req)
|
|
raise HTTPStatus::BadRequest
|
|
end
|
|
|
|
if auth_req['realm'] != @realm
|
|
error('%s: realm unmatch. "%s" for "%s"',
|
|
auth_req['username'], auth_req['realm'], @realm)
|
|
return false
|
|
end
|
|
|
|
auth_req['algorithm'] ||= 'MD5'
|
|
if auth_req['algorithm'].upcase != @algorithm.upcase
|
|
error('%s: algorithm unmatch. "%s" for "%s"',
|
|
auth_req['username'], auth_req['algorithm'], @algorithm)
|
|
return false
|
|
end
|
|
|
|
if (@qop.nil? && auth_req.has_key?('qop')) ||
|
|
(@qop && (! @qop.member?(auth_req['qop'])))
|
|
error('%s: the qop is not allowed. "%s"',
|
|
auth_req['username'], auth_req['qop'])
|
|
return false
|
|
end
|
|
|
|
password = @userdb.get_passwd(@realm, auth_req['username'], @reload_db)
|
|
unless password
|
|
error('%s: the user is not allowed.', auth_req['username'])
|
|
return false
|
|
end
|
|
|
|
nonce_is_invalid = false
|
|
if @use_opaque
|
|
info("@opaque = %s", @opaque.inspect) if $DEBUG
|
|
if !(opaque = auth_req['opaque'])
|
|
error('%s: opaque is not given.', auth_req['username'])
|
|
nonce_is_invalid = true
|
|
elsif !(opaque_struct = @opaques[opaque])
|
|
error('%s: invalid opaque is given.', auth_req['username'])
|
|
nonce_is_invalid = true
|
|
elsif !check_opaque(opaque_struct, req, auth_req)
|
|
@opaques.delete(auth_req['opaque'])
|
|
nonce_is_invalid = true
|
|
end
|
|
elsif !check_nonce(req, auth_req)
|
|
nonce_is_invalid = true
|
|
end
|
|
|
|
if /-sess$/i =~ auth_req['algorithm']
|
|
ha1 = hexdigest(password, auth_req['nonce'], auth_req['cnonce'])
|
|
else
|
|
ha1 = password
|
|
end
|
|
|
|
if auth_req['qop'] == "auth" || auth_req['qop'] == nil
|
|
ha2 = hexdigest(req.request_method, auth_req['uri'])
|
|
ha2_res = hexdigest("", auth_req['uri'])
|
|
elsif auth_req['qop'] == "auth-int"
|
|
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"
|
|
param2 = ['nonce', 'nc', 'cnonce', 'qop'].map{|key|
|
|
auth_req[key]
|
|
}.join(':')
|
|
digest = hexdigest(ha1, param2, ha2)
|
|
digest_res = hexdigest(ha1, param2, ha2_res)
|
|
else
|
|
digest = hexdigest(ha1, auth_req['nonce'], ha2)
|
|
digest_res = hexdigest(ha1, auth_req['nonce'], ha2_res)
|
|
end
|
|
|
|
if digest != auth_req['response']
|
|
error("%s: digest unmatch.", auth_req['username'])
|
|
return false
|
|
elsif nonce_is_invalid
|
|
error('%s: digest is valid, but nonce is not valid.',
|
|
auth_req['username'])
|
|
return :nonce_is_stale
|
|
elsif @use_auth_info_header
|
|
auth_info = {
|
|
'nextnonce' => generate_next_nonce(req),
|
|
'rspauth' => digest_res
|
|
}
|
|
if @use_opaque
|
|
opaque_struct.time = req.request_time
|
|
opaque_struct.nonce = auth_info['nextnonce']
|
|
opaque_struct.nc = "%08x" % (auth_req['nc'].hex + 1)
|
|
end
|
|
if auth_req['qop'] == "auth" || auth_req['qop'] == "auth-int"
|
|
['qop','cnonce','nc'].each{|key|
|
|
auth_info[key] = auth_req[key]
|
|
}
|
|
end
|
|
res[@resp_info_field] = auth_info.keys.map{|key|
|
|
if key == 'nc'
|
|
key + '=' + auth_info[key]
|
|
else
|
|
key + "=" + HTTPUtils::quote(auth_info[key])
|
|
end
|
|
}.join(', ')
|
|
end
|
|
info('%s: authentication succeeded.', auth_req['username'])
|
|
req.user = auth_req['username']
|
|
return true
|
|
end
|
|
|
|
def split_param_value(string)
|
|
ret = {}
|
|
while string.bytesize != 0
|
|
case string
|
|
when /^\s*([\w\-\.\*\%\!]+)=\s*\"((\\.|[^\"])*)\"\s*,?/
|
|
key = $1
|
|
matched = $2
|
|
string = $'
|
|
ret[key] = matched.gsub(/\\(.)/, "\\1")
|
|
when /^\s*([\w\-\.\*\%\!]+)=\s*([^,\"]*),?/
|
|
key = $1
|
|
matched = $2
|
|
string = $'
|
|
ret[key] = matched.clone
|
|
when /^s*^,/
|
|
string = $'
|
|
else
|
|
break
|
|
end
|
|
end
|
|
ret
|
|
end
|
|
|
|
def generate_next_nonce(req)
|
|
now = "%012d" % req.request_time.to_i
|
|
pk = hexdigest(now, @instance_key)[0,32]
|
|
nonce = [now + ":" + pk].pack("m0") # it has 60 length of chars.
|
|
nonce
|
|
end
|
|
|
|
def check_nonce(req, auth_req)
|
|
username = auth_req['username']
|
|
nonce = auth_req['nonce']
|
|
|
|
pub_time, pk = nonce.unpack("m*")[0].split(":", 2)
|
|
if (!pub_time || !pk)
|
|
error("%s: empty nonce is given", username)
|
|
return false
|
|
elsif (hexdigest(pub_time, @instance_key)[0,32] != pk)
|
|
error("%s: invalid private-key: %s for %s",
|
|
username, hexdigest(pub_time, @instance_key)[0,32], pk)
|
|
return false
|
|
end
|
|
|
|
diff_time = req.request_time.to_i - pub_time.to_i
|
|
if (diff_time < 0)
|
|
error("%s: difference of time-stamp is negative.", username)
|
|
return false
|
|
elsif diff_time > @nonce_expire_period
|
|
error("%s: nonce is expired.", username)
|
|
return false
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
def generate_opaque(req)
|
|
@mutex.synchronize{
|
|
now = req.request_time
|
|
if now - @last_nonce_expire > @nonce_expire_delta
|
|
@opaques.delete_if{|key,val|
|
|
(now - val.time) > @nonce_expire_period
|
|
}
|
|
@last_nonce_expire = now
|
|
end
|
|
begin
|
|
opaque = Utils::random_string(16)
|
|
end while @opaques[opaque]
|
|
@opaques[opaque] = OpaqueInfo.new(now, nil, '00000001')
|
|
opaque
|
|
}
|
|
end
|
|
|
|
def check_opaque(opaque_struct, req, auth_req)
|
|
if (@use_next_nonce && auth_req['nonce'] != opaque_struct.nonce)
|
|
error('%s: nonce unmatched. "%s" for "%s"',
|
|
auth_req['username'], auth_req['nonce'], opaque_struct.nonce)
|
|
return false
|
|
elsif !check_nonce(req, auth_req)
|
|
return false
|
|
end
|
|
if (@check_nc && auth_req['nc'] != opaque_struct.nc)
|
|
error('%s: nc unmatched."%s" for "%s"',
|
|
auth_req['username'], auth_req['nc'], opaque_struct.nc)
|
|
return false
|
|
end
|
|
true
|
|
end
|
|
|
|
def check_uri(req, auth_req)
|
|
uri = auth_req['uri']
|
|
if uri != req.request_uri.to_s && uri != req.unparsed_uri &&
|
|
(@internet_explorer_hack && uri != req.path)
|
|
error('%s: uri unmatch. "%s" for "%s"', auth_req['username'],
|
|
auth_req['uri'], req.request_uri.to_s)
|
|
return false
|
|
end
|
|
true
|
|
end
|
|
|
|
def hexdigest(*args)
|
|
@h.hexdigest(args.join(":"))
|
|
end
|
|
|
|
# :startdoc:
|
|
end
|
|
|
|
##
|
|
# Digest authentication for proxy servers. See DigestAuth for details.
|
|
|
|
class ProxyDigestAuth < DigestAuth
|
|
include ProxyAuthenticator
|
|
|
|
private
|
|
def check_uri(req, auth_req) # :nodoc:
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
end
|