diff --git a/lib/rack/utils.rb b/lib/rack/utils.rb index d2c8bc440..fc6e8a2b1 100644 --- a/lib/rack/utils.rb +++ b/lib/rack/utils.rb @@ -28,6 +28,14 @@ def unescape(s) DEFAULT_SEP = /[&;] */n + class << self + attr_accessor :key_space_limit + end + + # The default number of bytes to allow parameter keys to take up. + # This helps prevent a rogue client from flooding a Request. + self.key_space_limit = 65536 + # Stolen from Mongrel, with some small modifications: # Parses a query string by breaking it up at the '&' # and ';' characters. You can also use this to parse @@ -36,8 +44,19 @@ def unescape(s) def parse_query(qs, d = nil) params = {} + max_key_space = Utils.key_space_limit + bytes = 0 + (qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p| k, v = p.split('=', 2).map { |x| unescape(x) } + + if k + bytes += k.size + if bytes > max_key_space + raise RangeError, "exceeded available parameter key space" + end + end + if cur = params[k] if cur.class == Array params[k] << v @@ -56,8 +75,19 @@ def parse_query(qs, d = nil) def parse_nested_query(qs, d = nil) params = {} + max_key_space = Utils.key_space_limit + bytes = 0 + (qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p| k, v = unescape(p).split('=', 2) + + if k + bytes += k.size + if bytes > max_key_space + raise RangeError, "exceeded available parameter key space" + end + end + normalize_params(params, k, v) end @@ -489,6 +519,9 @@ def self.parse_multipart(env) rx = /(?:#{EOL})?#{Regexp.quote boundary}(#{EOL}|--)/n + max_key_space = Utils.key_space_limit + bytes = 0 + loop { head = nil body = '' @@ -503,6 +536,13 @@ def self.parse_multipart(env) content_type = head[/Content-Type: (.*)#{EOL}/ni, 1] name = head[/Content-Disposition:.*\s+name="?([^\";]*)"?/ni, 1] || head[/Content-ID:\s*([^#{EOL}]*)/ni, 1] + if name + bytes += name.size + if bytes > max_key_space + raise RangeError, "exceeded available parameter key space" + end + end + if content_type || filename body = Tempfile.new("RackMultipart") body.binmode if body.respond_to?(:binmode) diff --git a/test/spec_rack_request.rb b/test/spec_rack_request.rb index fcdeb4844..a34a9675f 100644 --- a/test/spec_rack_request.rb +++ b/test/spec_rack_request.rb @@ -79,6 +79,32 @@ req.params.should.equal "foo" => "bar", "quux" => "bla" end + specify "limit the keys from the GET query string" do + env = Rack::MockRequest.env_for("/?foo=bar") + + old, Rack::Utils.key_space_limit = Rack::Utils.key_space_limit, 1 + begin + req = Rack::Request.new(env) + lambda { req.GET }.should.raise(RangeError) + ensure + Rack::Utils.key_space_limit = old + end + end + + specify "limit the keys from the POST form data" do + env = Rack::MockRequest.env_for("", + "REQUEST_METHOD" => 'POST', + :input => "foo=bar&quux=bla") + + old, Rack::Utils.key_space_limit = Rack::Utils.key_space_limit, 1 + begin + req = Rack::Request.new(env) + lambda { req.POST }.should.raise(RangeError) + ensure + Rack::Utils.key_space_limit = old + end + end + specify "can parse POST data with explicit content type regardless of method" do req = Rack::Request.new \ Rack::MockRequest.env_for("/", @@ -295,6 +321,29 @@ req.media_type_params['bling'].should.equal 'bam' end + specify "raise RangeError if the key space is exhausted" do + input = < "multipart/form-data, boundary=AaB03x", + "CONTENT_LENGTH" => input.size, + :input => input) + + old, Rack::Utils.key_space_limit = Rack::Utils.key_space_limit, 1 + begin + lambda { Rack::Utils::Multipart.parse_multipart(env) }.should.raise(RangeError) + ensure + Rack::Utils.key_space_limit = old + end + end + specify "can parse multipart form data" do # Adapted from RFC 1867. input = <