diff --git a/lib/rack.rb b/lib/rack.rb index 6441e9927..48ac3e211 100644 --- a/lib/rack.rb +++ b/lib/rack.rb @@ -18,7 +18,7 @@ def self.version VERSION.join(".") end - RELEASE = "2.0.5" + RELEASE = "2.0.8" # Return the Rack release as a dotted string. def self.release diff --git a/lib/rack/multipart/parser.rb b/lib/rack/multipart/parser.rb index c02e26f6b..94f0fae39 100644 --- a/lib/rack/multipart/parser.rb +++ b/lib/rack/multipart/parser.rb @@ -5,7 +5,7 @@ module Multipart class MultipartPartLimitError < Errno::EMFILE; end class Parser - BUFSIZE = 1_048_576 + BUFSIZE = 16384 TEXT_PLAIN = "text/plain" TEMPFILE_FACTORY = lambda { |filename, content_type| Tempfile.new(["RackMultipart", ::File.extname(filename.gsub("\0".freeze, '%00'.freeze))]) @@ -39,8 +39,6 @@ def read(size) str end - def eof?; @content_length == @cursor; end - def rewind @io.rewind end @@ -65,11 +63,11 @@ def self.parse(io, content_length, content_type, tmpfile, bufsize, qp) io = BoundedIO.new(io, content_length) if content_length parser = new(boundary, tmpfile, bufsize, qp) - parser.on_read io.read(bufsize), io.eof? + parser.on_read io.read(bufsize) loop do break if parser.state == :DONE - parser.on_read io.read(bufsize), io.eof? + parser.on_read io.read(bufsize) end io.rewind @@ -181,8 +179,8 @@ def initialize(boundary, tempfile, bufsize, query_parser) @collector = Collector.new tempfile end - def on_read content, eof - handle_empty_content!(content, eof) + def on_read content + handle_empty_content!(content) @buf << content run_parser end @@ -358,10 +356,9 @@ def tag_multipart_encoding(filename, content_type, name, body) end - def handle_empty_content!(content, eof) + def handle_empty_content!(content) if content.nil? || content.empty? - raise EOFError if eof - return true + raise EOFError end end end diff --git a/lib/rack/request.rb b/lib/rack/request.rb index 2a00de704..6fcf6ee9e 100644 --- a/lib/rack/request.rb +++ b/lib/rack/request.rb @@ -11,6 +11,8 @@ module Rack # req.params["data"] class Request + SCHEME_WHITELIST = %w(https http).freeze + def initialize(env) @params = nil super(env) @@ -188,10 +190,8 @@ def scheme 'https' elsif get_header(HTTP_X_FORWARDED_SSL) == 'on' 'https' - elsif get_header(HTTP_X_FORWARDED_SCHEME) - get_header(HTTP_X_FORWARDED_SCHEME) - elsif get_header(HTTP_X_FORWARDED_PROTO) - get_header(HTTP_X_FORWARDED_PROTO).split(',')[0] + elsif forwarded_scheme + forwarded_scheme else get_header(RACK_URL_SCHEME) end @@ -261,7 +261,7 @@ def ip forwarded_ips = split_ip_addresses(get_header('HTTP_X_FORWARDED_FOR')) - return reject_trusted_ip_addresses(forwarded_ips).last || get_header("REMOTE_ADDR") + return reject_trusted_ip_addresses(forwarded_ips).last || forwarded_ips.first || get_header("REMOTE_ADDR") end # The media type (type/subtype) portion of the CONTENT_TYPE header @@ -479,6 +479,19 @@ def split_ip_addresses(ip_addresses) def reject_trusted_ip_addresses(ip_addresses) ip_addresses.reject { |ip| trusted_proxy?(ip) } end + + def forwarded_scheme + scheme_headers = [ + get_header(HTTP_X_FORWARDED_SCHEME), + get_header(HTTP_X_FORWARDED_PROTO).to_s.split(',')[0] + ] + + scheme_headers.each do |header| + return header if SCHEME_WHITELIST.include?(header) + end + + nil + end end include Env diff --git a/lib/rack/session/abstract/id.rb b/lib/rack/session/abstract/id.rb index 1bb8d5d06..9d63ca2dd 100644 --- a/lib/rack/session/abstract/id.rb +++ b/lib/rack/session/abstract/id.rb @@ -6,11 +6,38 @@ require 'rack/request' require 'rack/response' require 'securerandom' +require 'digest/sha2' module Rack module Session + class SessionId + ID_VERSION = 2 + + attr_reader :public_id + + def initialize(public_id) + @public_id = public_id + end + + def private_id + "#{ID_VERSION}::#{hash_sid(public_id)}" + end + + alias :cookie_value :public_id + + def empty?; false; end + def to_s; raise; end + def inspect; public_id.inspect; end + + private + + def hash_sid(sid) + Digest::SHA256.hexdigest(sid) + end + end + module Abstract # SessionHash is responsible to lazily load the session from store. @@ -357,7 +384,7 @@ def commit_session(req, res) req.get_header(RACK_ERRORS).puts("Deferring cookie for #{session_id}") if $VERBOSE else cookie = Hash.new - cookie[:value] = data + cookie[:value] = cookie_value(data) cookie[:expires] = Time.now + options[:expire_after] if options[:expire_after] cookie[:expires] = Time.now + options[:max_age] if options[:max_age] set_cookie(req, res, cookie.merge!(options)) @@ -365,6 +392,10 @@ def commit_session(req, res) end public :commit_session + def cookie_value(data) + data + end + # Sets the cookie back to the client with session id. We skip the cookie # setting if the value didn't change (sid is the same) or expires was given. @@ -406,6 +437,40 @@ def delete_session(req, sid, options) end end + class PersistedSecure < Persisted + class SecureSessionHash < SessionHash + def [](key) + if key == "session_id" + load_for_read! + id.public_id + else + super + end + end + end + + def generate_sid(*) + public_id = super + + SessionId.new(public_id) + end + + def extract_session_id(*) + public_id = super + public_id && SessionId.new(public_id) + end + + private + + def session_class + SecureSessionHash + end + + def cookie_value(data) + data.cookie_value + end + end + class ID < Persisted def self.inherited(klass) k = klass.ancestors.find { |kl| kl.respond_to?(:superclass) && kl.superclass == ID } diff --git a/lib/rack/session/cookie.rb b/lib/rack/session/cookie.rb index 71bb96f4f..90ed5cf17 100644 --- a/lib/rack/session/cookie.rb +++ b/lib/rack/session/cookie.rb @@ -45,7 +45,7 @@ module Session # }) # - class Cookie < Abstract::Persisted + class Cookie < Abstract::PersistedSecure # Encode session cookies as Base64 class Base64 def encode(str) @@ -153,6 +153,15 @@ def persistent_session_id!(data, sid=nil) data end + class SessionId < DelegateClass(Session::SessionId) + attr_reader :cookie_value + + def initialize(session_id, cookie_value) + super(session_id) + @cookie_value = cookie_value + end + end + def write_session(req, session_id, session, options) session = session.merge("session_id" => session_id) session_data = coder.encode(session) @@ -165,7 +174,7 @@ def write_session(req, session_id, session, options) req.get_header(RACK_ERRORS).puts("Warning! Rack::Session::Cookie data size exceeds 4K.") nil else - session_data + SessionId.new(session_id, session_data) end end diff --git a/lib/rack/session/memcache.rb b/lib/rack/session/memcache.rb index 4cf5ea09e..1f9d3ec56 100644 --- a/lib/rack/session/memcache.rb +++ b/lib/rack/session/memcache.rb @@ -19,7 +19,7 @@ module Session # Note that memcache does drop data before it may be listed to expire. For # a full description of behaviour, please see memcache's documentation. - class Memcache < Abstract::ID + class Memcache < Abstract::PersistedSecure attr_reader :mutex, :pool DEFAULT_OPTIONS = Abstract::ID::DEFAULT_OPTIONS.merge \ @@ -42,15 +42,15 @@ def initialize(app, options={}) def generate_sid loop do sid = super - break sid unless @pool.get(sid, true) + break sid unless @pool.get(sid.private_id, true) end end - def get_session(env, sid) - with_lock(env) do - unless sid and session = @pool.get(sid) + def find_session(req, sid) + with_lock(req) do + unless sid and session = get_session_with_fallback(sid) sid, session = generate_sid, {} - unless /^STORED/ =~ @pool.add(sid, session) + unless /^STORED/ =~ @pool.add(sid.private_id, session) raise "Session collision on '#{sid.inspect}'" end end @@ -58,25 +58,26 @@ def get_session(env, sid) end end - def set_session(env, session_id, new_session, options) + def write_session(req, session_id, new_session, options) expiry = options[:expire_after] expiry = expiry.nil? ? 0 : expiry + 1 - with_lock(env) do - @pool.set session_id, new_session, expiry + with_lock(req) do + @pool.set session_id.private_id, new_session, expiry session_id end end - def destroy_session(env, session_id, options) - with_lock(env) do - @pool.delete(session_id) + def delete_session(req, session_id, options) + with_lock(req) do + @pool.delete(session_id.public_id) + @pool.delete(session_id.private_id) generate_sid unless options[:drop] end end - def with_lock(env) - @mutex.lock if env[RACK_MULTITHREAD] + def with_lock(req) + @mutex.lock if req.multithread? yield rescue MemCache::MemCacheError, Errno::ECONNREFUSED if $VERBOSE @@ -88,6 +89,11 @@ def with_lock(env) @mutex.unlock if @mutex.locked? end + private + + def get_session_with_fallback(sid) + @pool.get(sid.private_id) || @pool.get(sid.public_id) + end end end end diff --git a/lib/rack/session/pool.rb b/lib/rack/session/pool.rb index 4c9c25c7a..6c0f66811 100644 --- a/lib/rack/session/pool.rb +++ b/lib/rack/session/pool.rb @@ -24,7 +24,7 @@ module Session # ) # Rack::Handler::WEBrick.run sessioned - class Pool < Abstract::Persisted + class Pool < Abstract::PersistedSecure attr_reader :mutex, :pool DEFAULT_OPTIONS = Abstract::ID::DEFAULT_OPTIONS.merge :drop => false @@ -37,15 +37,15 @@ def initialize(app, options={}) def generate_sid loop do sid = super - break sid unless @pool.key? sid + break sid unless @pool.key? sid.private_id end end def find_session(req, sid) with_lock(req) do - unless sid and session = @pool[sid] + unless sid and session = get_session_with_fallback(sid) sid, session = generate_sid, {} - @pool.store sid, session + @pool.store sid.private_id, session end [sid, session] end @@ -53,14 +53,15 @@ def find_session(req, sid) def write_session(req, session_id, new_session, options) with_lock(req) do - @pool.store session_id, new_session + @pool.store session_id.private_id, new_session session_id end end def delete_session(req, session_id, options) with_lock(req) do - @pool.delete(session_id) + @pool.delete(session_id.public_id) + @pool.delete(session_id.private_id) generate_sid unless options[:drop] end end @@ -71,6 +72,12 @@ def with_lock(req) ensure @mutex.unlock if @mutex.locked? end + + private + + def get_session_with_fallback(sid) + @pool[sid.private_id] || @pool[sid.public_id] + end end end end diff --git a/lib/rack/show_exceptions.rb b/lib/rack/show_exceptions.rb index ca86b2b2a..ef30fce43 100644 --- a/lib/rack/show_exceptions.rb +++ b/lib/rack/show_exceptions.rb @@ -46,7 +46,7 @@ def call(env) end def prefers_plaintext?(env) - !accepts_html(env) + !accepts_html?(env) end def accepts_html?(env) diff --git a/test/spec_request.rb b/test/spec_request.rb index bdad68fa7..6ed27ced0 100644 --- a/test/spec_request.rb +++ b/test/spec_request.rb @@ -572,6 +572,11 @@ def initialize(*) request.must_be :ssl? end + it "prevents scheme abuse" do + request = make_request(Rack::MockRequest.env_for("/", 'HTTP_X_FORWARDED_SCHEME' => 'a.">')) + request.scheme.must_equal 'http' + end + it "parse cookies" do req = make_request \ Rack::MockRequest.env_for("", "HTTP_COOKIE" => "foo=bar;quux=h&m") @@ -1281,7 +1286,16 @@ def ip_app res.body.must_equal '2.2.2.3' end - it "regard local addresses as proxies" do + it "preserves ip for trusted proxy chain" do + mock = Rack::MockRequest.new(Rack::Lint.new(ip_app)) + res = mock.get '/', + 'HTTP_X_FORWARDED_FOR' => '192.168.0.11, 192.168.0.7', + 'HTTP_CLIENT_IP' => '127.0.0.1' + res.body.must_equal '192.168.0.11' + + end + + it "regards local addresses as proxies" do req = make_request(Rack::MockRequest.env_for("/")) req.trusted_proxy?('127.0.0.1').must_equal 0 req.trusted_proxy?('10.0.0.1').must_equal 0 diff --git a/test/spec_session_memcache.rb b/test/spec_session_memcache.rb index 93a03d120..824db6836 100644 --- a/test/spec_session_memcache.rb +++ b/test/spec_session_memcache.rb @@ -226,15 +226,52 @@ req = Rack::MockRequest.new(pool) res0 = req.get("/") - session_id = (cookie = res0["Set-Cookie"])[session_match, 1] - ses0 = pool.pool.get(session_id, true) + session_id = Rack::Session::SessionId.new (cookie = res0["Set-Cookie"])[session_match, 1] + ses0 = pool.pool.get(session_id.private_id, true) req.get("/", "HTTP_COOKIE" => cookie) - ses1 = pool.pool.get(session_id, true) + ses1 = pool.pool.get(session_id.private_id, true) ses1.wont_equal ses0 end + it "can read the session with the legacy id" do + pool = Rack::Session::Memcache.new(incrementor) + req = Rack::MockRequest.new(pool) + + res0 = req.get("/") + cookie = res0["Set-Cookie"] + session_id = Rack::Session::SessionId.new cookie[session_match, 1] + ses0 = pool.pool.get(session_id.private_id, true) + pool.pool.set(session_id.public_id, ses0, 0, true) + pool.pool.delete(session_id.private_id) + + res1 = req.get("/", "HTTP_COOKIE" => cookie) + res1["Set-Cookie"].must_be_nil + res1.body.must_equal '{"counter"=>2}' + pool.pool.get(session_id.private_id, true).wont_be_nil + end + + it "drops the session in the legacy id as well" do + pool = Rack::Session::Memcache.new(incrementor) + req = Rack::MockRequest.new(pool) + drop = Rack::Utils::Context.new(pool, drop_session) + dreq = Rack::MockRequest.new(drop) + + res0 = req.get("/") + cookie = res0["Set-Cookie"] + session_id = Rack::Session::SessionId.new cookie[session_match, 1] + ses0 = pool.pool.get(session_id.private_id, true) + pool.pool.set(session_id.public_id, ses0, 0, true) + pool.pool.delete(session_id.private_id) + + res2 = dreq.get("/", "HTTP_COOKIE" => cookie) + res2["Set-Cookie"].must_be_nil + res2.body.must_equal '{"counter"=>2}' + pool.pool.get(session_id.private_id, true).must_be_nil + pool.pool.get(session_id.public_id, true).must_be_nil + end + # anyone know how to do this better? it "cleanly merges sessions when multithreaded" do skip unless $DEBUG diff --git a/test/spec_session_pool.rb b/test/spec_session_pool.rb index 2d0616915..62a0505e7 100644 --- a/test/spec_session_pool.rb +++ b/test/spec_session_pool.rb @@ -6,7 +6,7 @@ describe Rack::Session::Pool do session_key = Rack::Session::Pool::DEFAULT_OPTIONS[:key] - session_match = /#{session_key}=[0-9a-fA-F]+;/ + session_match = /#{session_key}=([0-9a-fA-F]+);/ incrementor = lambda do |env| env["rack.session"]["counter"] ||= 0 @@ -14,7 +14,7 @@ Rack::Response.new(env["rack.session"].inspect).to_a end - session_id = Rack::Lint.new(lambda do |env| + get_session_id = Rack::Lint.new(lambda do |env| Rack::Response.new(env["rack.session"].inspect).to_a end) @@ -143,6 +143,43 @@ pool.pool.size.must_equal 1 end + it "can read the session with the legacy id" do + pool = Rack::Session::Pool.new(incrementor) + req = Rack::MockRequest.new(pool) + + res0 = req.get("/") + cookie = res0["Set-Cookie"] + session_id = Rack::Session::SessionId.new cookie[session_match, 1] + ses0 = pool.pool[session_id.private_id] + pool.pool[session_id.public_id] = ses0 + pool.pool.delete(session_id.private_id) + + res1 = req.get("/", "HTTP_COOKIE" => cookie) + res1["Set-Cookie"].must_be_nil + res1.body.must_equal '{"counter"=>2}' + pool.pool[session_id.private_id].wont_be_nil + end + + it "drops the session in the legacy id as well" do + pool = Rack::Session::Pool.new(incrementor) + req = Rack::MockRequest.new(pool) + drop = Rack::Utils::Context.new(pool, drop_session) + dreq = Rack::MockRequest.new(drop) + + res0 = req.get("/") + cookie = res0["Set-Cookie"] + session_id = Rack::Session::SessionId.new cookie[session_match, 1] + ses0 = pool.pool[session_id.private_id] + pool.pool[session_id.public_id] = ses0 + pool.pool.delete(session_id.private_id) + + res2 = dreq.get("/", "HTTP_COOKIE" => cookie) + res2["Set-Cookie"].must_be_nil + res2.body.must_equal '{"counter"=>2}' + pool.pool[session_id.private_id].must_be_nil + pool.pool[session_id.public_id].must_be_nil + end + # anyone know how to do this better? it "should merge sessions when multithreaded" do unless $DEBUG @@ -191,7 +228,7 @@ end it "does not return a cookie if cookie was not written (only read)" do - app = Rack::Session::Pool.new(session_id) + app = Rack::Session::Pool.new(get_session_id) res = Rack::MockRequest.new(app).get("/") res["Set-Cookie"].must_be_nil end diff --git a/test/spec_show_exceptions.rb b/test/spec_show_exceptions.rb index cd44c8168..61e5d92d6 100644 --- a/test/spec_show_exceptions.rb +++ b/test/spec_show_exceptions.rb @@ -77,4 +77,17 @@ def show_exceptions(app) assert_match(res, /ShowExceptions/) assert_match(res, /unknown location/) end + + it "knows to prefer plaintext for non-html" do + # We don't need an app for this + exc = Rack::ShowExceptions.new(nil) + + [ + [{ "HTTP_ACCEPT" => "text/plain" }, true], + [{ "HTTP_ACCEPT" => "text/foo" }, true], + [{ "HTTP_ACCEPT" => "text/html" }, false] + ].each do |env, expected| + assert_equal(expected, exc.prefers_plaintext?(env)) + end + end end
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: