From c6ffae22748bc22d5ef88fea2a3ca67f480ee74b Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Fri, 19 Nov 2010 10:19:45 +0000 Subject: max_body: rewrite wrappers to be safer To avoid denial-of-service attacks, the wrappers need to intercept requests *before* they hit the memory allocator, so we need to reimplement the read(all) and gets cases to use smaller buffers whenever the application does not specify one. --- lib/rainbows/max_body/rewindable_wrapper.rb | 1 + lib/rainbows/max_body/wrapper.rb | 66 +++++++++++--- t/sha1-random-size.ru | 24 +++-- t/t0104-rack-input-limit-tiny.sh | 134 +++++++++++++++++++++++++++- t/t0105-rack-input-limit-bigger.sh | 2 + 5 files changed, 210 insertions(+), 17 deletions(-) diff --git a/lib/rainbows/max_body/rewindable_wrapper.rb b/lib/rainbows/max_body/rewindable_wrapper.rb index b52726e..5693ead 100644 --- a/lib/rainbows/max_body/rewindable_wrapper.rb +++ b/lib/rainbows/max_body/rewindable_wrapper.rb @@ -8,6 +8,7 @@ class Rainbows::MaxBody::RewindableWrapper < Rainbows::MaxBody::Wrapper def rewind @limit = @orig_limit + @rbuf = '' @input.rewind end diff --git a/lib/rainbows/max_body/wrapper.rb b/lib/rainbows/max_body/wrapper.rb index 3c38ca6..15faeeb 100644 --- a/lib/rainbows/max_body/wrapper.rb +++ b/lib/rainbows/max_body/wrapper.rb @@ -1,26 +1,70 @@ # -*- encoding: binary -*- # :enddoc: +# +# This is only used for chunked request bodies, which are rare class Rainbows::MaxBody::Wrapper def initialize(rack_input, limit) - @input, @limit = rack_input, limit - end - - def check(rv) - throw :rainbows_EFBIG if rv && ((@limit -= rv.size) < 0) - rv + @input, @limit, @rbuf = rack_input, limit, '' end def each(&block) - while line = @input.gets - yield check(line) + while line = gets + yield line end end - def read(*args) - check(@input.read(*args)) + # chunked encoding means this method behaves more like readpartial, + # since Rack does not support a method named "readpartial" + def read(length = nil, rv = '') + if length + if length <= @rbuf.size + length < 0 and raise ArgumentError, "negative length #{length} given" + rv.replace(@rbuf.slice!(0, length)) + elsif @rbuf.empty? + checked_read(length, rv) or return + else + rv.replace(@rbuf.slice!(0, @rbuf.size)) + end + rv.empty? && length != 0 ? nil : rv + else + rv.replace(read_all) + end end def gets - check(@input.gets) + sep = $/ + if sep.nil? + rv = read_all + return rv.empty? ? nil : rv + end + re = /\A(.*?#{Regexp.escape(sep)})/ + + begin + @rbuf.sub!(re, '') and return $1 + + if tmp = checked_read(16384) + @rbuf << tmp + elsif @rbuf.empty? # EOF + return nil + else # EOF, return whatever is left + return @rbuf.slice!(0, @rbuf.size) + end + end while true + end + + def checked_read(length = 16384, buf = '') + if @input.read(length, buf) + throw :rainbows_EFBIG if ((@limit -= buf.size) < 0) + return buf + end + end + + def read_all + rv = @rbuf.slice!(0, @rbuf.size) + tmp = '' + while checked_read(16384, tmp) + rv << tmp + end + rv end end diff --git a/t/sha1-random-size.ru b/t/sha1-random-size.ru index f86d017..4ef5d7b 100644 --- a/t/sha1-random-size.ru +++ b/t/sha1-random-size.ru @@ -7,11 +7,25 @@ app = lambda do |env| return [ 100, {}, [] ] digest = Digest::SHA1.new input = env['rack.input'] - if buf = input.read(rand(cap)) - begin - raise "#{buf.size} > #{cap}" if buf.size > cap - digest.update(buf) - end while input.read(rand(cap), buf) + case env["PATH_INFO"] + when "/gets_read_mix" + warn "GETS_READ_MIX #{env['HTTP_TRANSFER_ENCODING'].inspect}" + if buf = input.gets + warn "input.rbuf: #{input.instance_variable_get(:@rbuf).inspect}" + begin + digest.update(buf) + warn "buf.size : #{buf.size}" + end while input.read(rand(cap), buf) + end + when "/each" + input.each { |buf| digest.update(buf) } + else + if buf = input.read(rand(cap)) + begin + raise "#{buf.size} > #{cap}" if buf.size > cap + digest.update(buf) + end while input.read(rand(cap), buf) + end end [ 200, {'Content-Type' => 'text/plain'}, [ digest.hexdigest << "\n" ] ] diff --git a/t/t0104-rack-input-limit-tiny.sh b/t/t0104-rack-input-limit-tiny.sh index 0cf9f73..284d7c0 100755 --- a/t/t0104-rack-input-limit-tiny.sh +++ b/t/t0104-rack-input-limit-tiny.sh @@ -3,7 +3,7 @@ test -r random_blob || die "random_blob required, run with 'make $0'" req_curl_chunked_upload_err_check -t_plan 6 "rack.input client_max_body_size tiny" +t_plan 18 "rack.input client_max_body_size tiny" t_begin "setup and startup" && { rtmpfiles curl_out curl_err cmbs_config @@ -21,6 +21,7 @@ t_begin "stops a regular request" && { http://$listen/ > $curl_out 2> $curl_err || > $ok dbgcat curl_err dbgcat curl_out + grep 413 $curl_err test -e $ok } @@ -31,6 +32,7 @@ t_begin "stops a large chunked request" && { http://$listen/ > $curl_out 2> $curl_err || > $ok dbgcat curl_err dbgcat curl_out + grep 413 $curl_err test -e $ok } @@ -56,6 +58,136 @@ t_begin "small size sha1 content-length ok" && { test "$(cat $curl_out)" = $blob_sha1 } +t_begin "stops a regular request (gets_read_mix)" && { + rm -f $ok + dd if=/dev/zero bs=257 count=1 of=$tmp + curl -vsSf -T $tmp -H Expect: \ + http://$listen/gets_read_mix > $curl_out 2> $curl_err || > $ok + dbgcat curl_err + dbgcat curl_out + grep 413 $curl_err + test -e $ok +} + +t_begin "stops a large chunked request (gets_read_mix)" && { + rm -f $ok + dd if=/dev/zero bs=257 count=1 | \ + curl -vsSf -T- -H Expect: \ + http://$listen/gets_read_mix > $curl_out 2> $curl_err || > $ok + dbgcat curl_err + dbgcat curl_out + grep 413 $curl_err + test -e $ok +} + +t_begin "stops a large line-based chunked request (gets_read_mix)" && { + rm -f $ok + =0;) print "hello world"}' | \ + curl -vsSf -T- -H Expect: \ + http://$listen/gets_read_mix > $curl_out 2> $curl_err || > $ok + dbgcat curl_err + dbgcat curl_out + grep 413 $curl_err + test -e $ok +} + +t_begin "OK with line-based chunked request (gets_read_mix)" && { + rm -f $ok + =0;) print "hello world"}' | \ + curl -vsSf -T- -H Expect: \ + http://$listen/gets_read_mix > $curl_out 2> $curl_err + dbgcat curl_err + dbgcat curl_out + test x"$(cat $curl_out)" = x23eab3cebcbe22a0456c8462e3d3bb01ae761702 +} + +t_begin "small size sha1 chunked ok (gets_read_mix)" && { + blob_sha1=b376885ac8452b6cbf9ced81b1080bfd570d9b91 + rm -f $ok + dd if=/dev/zero bs=256 count=1 | \ + curl -vsSf -T- -H Expect: \ + http://$listen/gets_read_mix > $curl_out 2> $curl_err + dbgcat curl_err + dbgcat curl_out + test "$(cat $curl_out)" = $blob_sha1 +} + +t_begin "small size sha1 content-length ok (gets_read_mix)" && { + blob_sha1=b376885ac8452b6cbf9ced81b1080bfd570d9b91 + rm -f $ok + dd if=/dev/zero bs=256 count=1 of=$tmp + curl -vsSf -T $tmp -H Expect: \ + http://$listen/gets_read_mix > $curl_out 2> $curl_err + dbgcat curl_err + dbgcat curl_out + test "$(cat $curl_out)" = $blob_sha1 +} + +t_begin "stops a regular request (each)" && { + rm -f $ok + dd if=/dev/zero bs=257 count=1 of=$tmp + curl -vsSf -T $tmp -H Expect: \ + http://$listen/each > $curl_out 2> $curl_err || > $ok + dbgcat curl_err + dbgcat curl_out + grep 413 $curl_err + test -e $ok +} + +t_begin "stops a large chunked request (each)" && { + rm -f $ok + dd if=/dev/zero bs=257 count=1 | \ + curl -vsSf -T- -H Expect: \ + http://$listen/each > $curl_out 2> $curl_err || > $ok + dbgcat curl_err + dbgcat curl_out + grep 413 $curl_err + test -e $ok +} + +t_begin "small size sha1 chunked ok (each)" && { + blob_sha1=b376885ac8452b6cbf9ced81b1080bfd570d9b91 + rm -f $ok + dd if=/dev/zero bs=256 count=1 | \ + curl -vsSf -T- -H Expect: \ + http://$listen/each > $curl_out 2> $curl_err + dbgcat curl_err + dbgcat curl_out + test "$(cat $curl_out)" = $blob_sha1 +} + +t_begin "small size sha1 content-length ok (each)" && { + blob_sha1=b376885ac8452b6cbf9ced81b1080bfd570d9b91 + rm -f $ok + dd if=/dev/zero bs=256 count=1 of=$tmp + curl -vsSf -T $tmp -H Expect: \ + http://$listen/each > $curl_out 2> $curl_err + dbgcat curl_err + dbgcat curl_out + test "$(cat $curl_out)" = $blob_sha1 +} + +t_begin "stops a large line-based chunked request (each)" && { + rm -f $ok + =0;) print "hello world"}' | \ + curl -vsSf -T- -H Expect: \ + http://$listen/each > $curl_out 2> $curl_err || > $ok + dbgcat curl_err + dbgcat curl_out + grep 413 $curl_err + test -e $ok +} + +t_begin "OK with line-based chunked request (each)" && { + rm -f $ok + =0;) print "hello world"}' | \ + curl -vsSf -T- -H Expect: \ + http://$listen/each > $curl_out 2> $curl_err + dbgcat curl_err + dbgcat curl_out + test x"$(cat $curl_out)" = x23eab3cebcbe22a0456c8462e3d3bb01ae761702 +} + t_begin "shutdown" && { kill $rainbows_pid } diff --git a/t/t0105-rack-input-limit-bigger.sh b/t/t0105-rack-input-limit-bigger.sh index e8cf95a..642dd2b 100755 --- a/t/t0105-rack-input-limit-bigger.sh +++ b/t/t0105-rack-input-limit-bigger.sh @@ -22,6 +22,7 @@ t_begin "stops a regular request" && { rm -f $tmp dbgcat curl_err dbgcat curl_out + grep 413 $curl_err test -e $ok } @@ -32,6 +33,7 @@ t_begin "stops a large chunked request" && { http://$listen/ > $curl_out 2> $curl_err || > $ok dbgcat curl_err dbgcat curl_out + grep 413 $curl_err test -e $ok } -- cgit v1.2.3-24-ge0c7