CVE-2016-4442 (Ruby, published 2017-05-02T14:59Z)
Description: value: The rack-mini-profiler gem before 0.10.1 for Ruby allows remote attackers to obtain sensitive information about allocated strings and objects by leveraging incorrect ordering of security checks.
Severity: MEDIUM (CVSS v3 base score: 5.3)
Weakness: CWE-200 \u2014 Exposure of Sensitive Information to an Unauthorized Actor
The product exposes sensitive information to an actor that is not explicitly authorized to have access to that information.
Repository: https://github.com/MiniProfiler/rack-mini-profiler
Files changed:
Gemfile
Guardfile
lib/mini_profiler/client_settings.rb
lib/mini_profiler/context.rb
lib/mini_profiler/profiler.rb
lib/mini_profiler/storage/abstract_store.rb
lib/mini_profiler/storage/file_store.rb
lib/mini_profiler/storage/memcache_store.rb
lib/mini_profiler/storage/memory_store.rb
lib/mini_profiler/storage/redis_store.rb
lib/rack-mini-profiler.rb
rack-mini-profiler.gemspec
spec/integration/mini_profiler_spec.rb
spec/lib/client_settings_spec.rb
spec/lib/storage/file_store_spec.rb
spec/lib/storage/memcache_store_spec.rb
spec/lib/storage/memory_store_spec.rb
spec/lib/storage/redis_store_spec.rb
spec/spec_helper.rb
Commit message: FEATURE: perform security checks earlier in the pipeline

This avoids needing to discard results and ensures no areas of mini profiler
can execute prior to a request being authorized

authorization token lasts for 30 minutes and is cycled after that


Vulnerable code:
gem 'codecov', :require => false, :group => :test
Autotest.add_discovery { "rspec2" }

      def initialize(env)
        @backtrace_level = nil if !@backtrace_level.nil? && (@backtrace_level == 0 || @backtrace_level > BACKTRACE_NONE)
        if @orig_disable_profiling != @disable_profiling || @orig_backtrace_level != @backtrace_level || @cookie.nil?
          settings["dp"] = "t"              if @disable_profiling
          settings["bt"] = @backtrace_level if @backtrace_level
        Rack::Utils.delete_cookie_header!(headers, COOKIE_NAME, :path => '/')
      def has_cookie?
        !@cookie.nil?
  attr_accessor :inject_js,:current_timer,:page_struct,:skip_backtrace,:full_backtrace,:discard, :mpt_init, :measure
      client_settings = ClientSettings.new(env)
      has_profiling_cookie = client_settings.has_cookie?

      if skip_it || (@config.authorization_mode == :whitelist && !has_profiling_cookie)
        status,headers,body = @app.call(env)
        if !skip_it && @config.authorization_mode == :whitelist && !has_profiling_cookie && MiniProfiler.request_authorized?
          client_settings.write!(headers)
        end
        return [status,headers,body]
      return serve_html(env) if path.start_with? @config.base_url_path
      if query_string =~ /pp=enable/ && (@config.authorization_mode != :whitelist || MiniProfiler.request_authorized?)
        client_settings.write!(headers)
        return [status,headers,body]
        return Rack::MiniProfiler::GCProfiler.new.profile_gc(@app, env)
        return text_result(result.string)
      MiniProfiler.deauthorize_request if @config.authorization_mode == :whitelist
      start = Time.now
        client_settings.write!(headers)
        # this is non-obvious, don't kill the profiling cookie on errors or short requests
        # this ensures that stuff that never reaches the rails stack does not kill profiling
        if status.to_i >= 200 && status.to_i < 300 && ((Time.now - start) > 0.1)
          client_settings.discard_cookie!(headers)
        end
      return [status,headers,body] if skip_it
        return dump_exceptions exceptions
        return dump_env env
        return analyze_memory
        return help(client_settings, env)
        return self.flamegraph(flamegraph)
          client_settings.write!(headers)
          return result if result
      client_settings.write!(headers)
      [status, headers, body]
      client_settings.write!(headers)
  s.add_development_dependency 'ZenTest'
  s.add_development_dependency 'autotest'
require 'spec_helper'

describe Rack::MiniProfiler::ClientSettings do

  describe "with settings" do
    before do
      settings = URI.encode_www_form_component("dp=t,bt=1")
      @settings = Rack::MiniProfiler::ClientSettings.new({"HTTP_COOKIE" => "__profilin=#{settings};" })
    end

    it 'has the cookies' do
      @settings.has_cookie?.should be_true
    end

    it 'has profiling disabled' do
      @settings.disable_profiling?.should be_true
    end

    it 'has backtrace set to full' do
      @settings.backtrace_full?.should be_true
    end

    it 'should not write cookie changes if no change' do
      hash = {}
      @settings.write!(hash)
      hash.should == {}
    end

    it 'should correctly write cookie changes if changed' do
      @settings.disable_profiling = false
      hash = {}
      @settings.write!(hash)
      hash.should_not == {}
    end
  end

  it "should not have settings by default" do
    Rack::MiniProfiler::ClientSettings.new({}).has_cookie?.should == false
  end


end
      id = ::JSON.parse(h)[0]
        expect(Rack::MiniProfiler).to receive(:request_authorized?) { true }.twice
        get '/html?pp=enable'
      set_cookie("__profilin=stylin")

Fixed code:
gem 'codecov', :require => false, :group => :test
# A sample Guardfile
# More info at https://github.com/guard/guard#readme

directories %w(lib spec) \
 .select{|d| Dir.exists?(d) ? d : UI.warning("Directory #{d} does not exist")}

## Note: if you are using the `directories` clause above and you are not
## watching the project directory ('.'), then you will want to move
## the Guardfile to a watched dir and symlink it back, e.g.
#
#  $ mkdir config
#  $ mv Guardfile config/
#  $ ln -s config/Guardfile .
#
# and, you'll have to watch "config/Guardfile" instead of "Guardfile"

# Note: The cmd option is now required due to the increasing number of ways
#       rspec may be run, below are examples of the most common uses.
#  * bundler: 'bundle exec rspec'
#  * bundler binstubs: 'bin/rspec'
#  * spring: 'bin/rsspec' (This will use spring if running and you have
#                          installed the spring binstubs per the docs)
#  * zeus: 'zeus rspec' (requires the server to be started separetly)
#  * 'just' rspec: 'rspec'
guard :rspec, cmd: 'bundle exec rspec', failed_mode: :focus do
  watch(%r{^spec/.+_spec\.rb$})
  watch(%r{^lib/mini_profiler/(.+)\.rb$})     { |m| "spec/lib/#{m[1]}_spec.rb" }
  watch('spec/spec_helper.rb')  { "spec" }
end

      def initialize(env, store, start)
        @store = store
        @start = start

        @allowed_tokens, @orig_auth_tokens = nil

            @orig_auth_tokens = v.to_s.split("|") if k == "a"
        if !@backtrace_level.nil? && (@backtrace_level == 0 || @backtrace_level > BACKTRACE_NONE)
          @backtrace_level = nil
        end

      def handle_cookie(result)
        status,headers,_body = result

        if (MiniProfiler.config.authorization_mode == :whitelist && !MiniProfiler.request_authorized?)
          # this is non-obvious, don't kill the profiling cookie on errors or short requests
          # this ensures that stuff that never reaches the rails stack does not kill profiling
          if status.to_i >= 200 && status.to_i < 300 && ((Time.now - @start) > 0.1)
            discard_cookie!(headers)
          end
        else
          write!(headers)
        end

        result
      end


        tokens_changed = false

        if MiniProfiler.request_authorized? && MiniProfiler.config.authorization_mode == :whitelist
          @allowed_tokens ||= @store.allowed_tokens
          tokens_changed = !@orig_auth_tokens || ((@allowed_tokens - @orig_auth_tokens).length > 0)
        end

        if  @orig_disable_profiling != @disable_profiling ||
            @orig_backtrace_level != @backtrace_level ||
            @cookie.nil? ||
            tokens_changed

          settings["dp"] = "t"                  if @disable_profiling
          settings["bt"] = @backtrace_level     if @backtrace_level
          settings["a"] = @allowed_tokens.join("|") if @allowed_tokens && MiniProfiler.request_authorized?

        if @cookie
          Rack::Utils.delete_cookie_header!(headers, COOKIE_NAME, :path => '/')
        end
      def has_valid_cookie?
        valid_cookie = !@cookie.nil?

        if (MiniProfiler.config.authorization_mode == :whitelist)
          @allowed_tokens ||= @store.allowed_tokens

          valid_cookie = (Array === @orig_auth_tokens) &&
            ((@allowed_tokens & @orig_auth_tokens).length > 0)
        end

        valid_cookie

  attr_accessor :inject_js,:current_timer,:page_struct,:skip_backtrace,
                :full_backtrace,:discard, :mpt_init, :measure
      start = Time.now
      client_settings = ClientSettings.new(env, @storage, start)
      MiniProfiler.deauthorize_request if @config.authorization_mode == :whitelist
      if skip_it || (
        @config.authorization_mode == :whitelist &&
        !client_settings.has_valid_cookie?
      )
        return client_settings.handle_cookie(@app.call(env))
      return client_settings.handle_cookie(serve_html(env)) if path.start_with? @config.base_url_path
      if query_string =~ /pp=enable/
        return client_settings.handle_cookie([status,headers,body])
        return client_settings.handle_cookie(Rack::MiniProfiler::GCProfiler.new.profile_gc(@app, env))
        return client_settings.handle_cookie(text_result(result.string))
      return client_settings.handle_cookie([status,headers,body]) if skip_it
        return client_settings.handle_cookie(dump_exceptions exceptions)
        return client_settings.handle_cookie(dump_env env)
        return client_settings.handle_cookie(analyze_memory)
        return client_settings.handle_cookie(help(client_settings, env))
        return client_settings.handle_cookie(self.flamegraph(flamegraph))
          return client_settings.handle_cookie(result) if result
      client_settings.handle_cookie([status, headers, body])
      # maximum age of allowed tokens before cycling in seconds
      MAX_TOKEN_AGE = 1800

      # a list of tokens that are permitted to access profiler in whitelist mode
      def allowed_tokens
        raise NotImplementedError.new("allowed_tokens is not implemented")
      end

        @auth_token_cache    = FileCache.new(@path, "tokens")
        @auth_token_lock     = Mutex.new

      def flush_tokens
        @auth_token_lock.synchronize {
          @auth_token_cache[""] = nil
        }
      end

      def allowed_tokens
        @auth_token_lock.synchronize {
          token1, token2, cycle_at = @auth_token_cache[""]

          unless cycle_at && (Time === cycle_at) && (cycle_at > Time.now)
            token2 = token1
            token1 = SecureRandom.hex
            cycle_at = Time.now + Rack::MiniProfiler::AbstractStore::MAX_TOKEN_AGE
          end

          @auth_token_cache[""] = [token1, token2, cycle_at]

          [token1, token2].compact
        }
      end

      def flush_tokens
        @client.set("#{@prefix}-tokens", nil)
      end

      def allowed_tokens

        token_info = @client.get("#{@prefix}-tokens")
        key1, key2, cycle_at = nil


        if token_info
           key1, key2, cycle_at = Marshal::load(token_info)

           key1 = nil unless key1 && key1.length == 32
           key2 = nil unless key2 && key2.length == 32

           if key1 && cycle_at && (cycle_at > Time.now)
              return [key1,key2].compact
           end
        end

        timeout = Rack::MiniProfiler::AbstractStore::MAX_TOKEN_AGE

        # cycle keys
        key2 = key1
        key1 = SecureRandom.hex
        cycle_at = Time.now + timeout

        @client.set("#{@prefix}-tokens", Marshal::dump([key1, key2, cycle_at]))

        [key1, key2].compact
      end


        @token1, @token2, @cycle_tokens_at = nil

        @token_lock = Mutex.new

      def allowed_tokens
        @token_lock.synchronize do

          unless @cycle_at && (@cycle_at > Time.now)
            @token2 = @token1
            @token1 = SecureRandom.hex
            @cycle_at = Time.now + Rack::MiniProfiler::AbstractStore::MAX_TOKEN_AGE
          end

          [@token1, @token2].compact

        end
      end
      attr_reader :prefix

      def flush_tokens
        redis.del("#{@prefix}-key1", "#{@prefix}-key1_old", "#{@prefix}-key2")
      end

      # Only used for testing
      def simulate_expire
        redis.del("#{@prefix}-key1")
      end

      def allowed_tokens
        key1, key1_old, key2 = redis.mget("#{@prefix}-key1", "#{@prefix}-key1_old", "#{@prefix}-key2")

        if key1 && (key1.length == 32)
          return [key1, key2].compact
        end

        timeout = Rack::MiniProfiler::AbstractStore::MAX_TOKEN_AGE

        # TODO  this could be moved to lua to correct a concurrency flaw
        # it is not critical cause worse case some requests will miss profiling info

        # no key so go ahead and set it
        key1 = SecureRandom.hex

        if key1_old && (key1_old.length == 32)
          key2 = key1_old
          redis.setex "#{@prefix}-key2", timeout, key2
        else
          key2 = nil
        end

        redis.setex "#{@prefix}-key1", timeout, key1
        redis.setex "#{@prefix}-key1_old", timeout*2, key1

        [key1, key2].compact
      end

require 'securerandom'
  s.add_development_dependency 'guard'
  s.add_development_dependency 'guard-rspec'
      map '/whitelisted-html' do
        run lambda { |env|
          Rack::MiniProfiler.authorize_request
          [200, {'Content-Type' => 'text/html'}, "<html><BODY><h1>Hi</h1></BODY>\n \t</html>"]
        }
      end
      _id = ::JSON.parse(h)[0]
        get '/html?pp=enable'
        get '/whitelisted-html?pp=enable'
        get '/whitelisted-html?pp=enable'
      get '/whitelisted'
      # second time will ensure cookie is set
      # first time around there is no cookie, so no profiling
require 'spec_helper'
require 'rack'

describe Rack::MiniProfiler::ClientSettings do

  describe "with settings" do
    before do
      @store = Rack::MiniProfiler::MemoryStore.new
      settings = URI.encode_www_form_component("dp=t,bt=1")
      @settings = Rack::MiniProfiler::ClientSettings.new(
        {"HTTP_COOKIE" => "__profilin=#{settings};" },
        @store,
        Time.now
      )
    end

    it 'has the cookies' do
      @settings.has_valid_cookie?.should be_true
    end

    it 'has profiling disabled' do
      @settings.disable_profiling?.should be_true
    end

    it 'has backtrace set to full' do
      @settings.backtrace_full?.should be_true
    end

    it 'should not write cookie changes if no change' do
      hash = {}
      @settings.write!(hash)
      hash.should == {}
    end

    it 'should correctly write cookie changes if changed' do
      @settings.disable_profiling = false
      hash = {}
      @settings.write!(hash)
      hash.should_not == {}
    end

    it 'writes auth token for authorized reqs' do
      Rack::MiniProfiler.config.authorization_mode = :whitelist
      Rack::MiniProfiler.authorize_request
      hash = {}
      @settings.write!(hash)
      hash["Set-Cookie"].should include(@store.allowed_tokens.join("|"))
    end

    it 'does nothing on short unauthed requests' do
      Rack::MiniProfiler.config.authorization_mode = :whitelist
      Rack::MiniProfiler.deauthorize_request
      hash = {}
      @settings.handle_cookie([200, hash, []])

      hash.should == {}
    end

    it 'discards on long unauthed requests' do
      Rack::MiniProfiler.config.authorization_mode = :whitelist
      Rack::MiniProfiler.deauthorize_request
      hash = {}
      Time.travel(Time.now + 1) do
        @settings.handle_cookie([200, hash, []])
      end

      hash["Set-Cookie"].should include("max-age=0")
    end
  end

  it "should not have settings by default" do
    Rack::MiniProfiler::ClientSettings.new({}, Rack::MiniProfiler::MemoryStore.new, Time.now)
      .has_valid_cookie?.should == false
  end


end

    describe 'allowed_tokens' do

      it 'should return tokens' do
        @store.flush_tokens

        tokens = @store.allowed_tokens
        tokens.length.should == 1
        tokens.should == @store.allowed_tokens

        Time.travel(Time.now + 1) do
          new_tokens = @store.allowed_tokens
          new_tokens.length.should == 1
          new_tokens.should == tokens
        end

        Time.travel(Time.now + Rack::MiniProfiler::AbstractStore::MAX_TOKEN_AGE + 1) do
          new_tokens = @store.allowed_tokens
          new_tokens.length.should == 2
          (new_tokens - tokens).length.should == 1
        end

      end
    end


  describe 'allowed_tokens' do
    before do
      @store = Rack::MiniProfiler::MemcacheStore.new
    end

    it 'should return tokens' do

      @store.flush_tokens

      tokens = @store.allowed_tokens
      tokens.length.should == 1
      tokens.should == @store.allowed_tokens

      Time.travel(Time.now + 1) do
        new_tokens = @store.allowed_tokens
        new_tokens.length.should == 1
        new_tokens.should == tokens
      end

      Time.travel(Time.now + Rack::MiniProfiler::AbstractStore::MAX_TOKEN_AGE + 1) do
        new_tokens = @store.allowed_tokens
        new_tokens.length.should == 2
        (new_tokens - tokens).length.should == 1
      end
    end
  end

  describe 'allowed_tokens' do
    before do
      @store = Rack::MiniProfiler::MemoryStore.new
    end

    it 'should return tokens' do

      tokens = @store.allowed_tokens
      tokens.length.should == 1
      tokens.should == @store.allowed_tokens

      Time.travel(Time.now + 1) do
        new_tokens = @store.allowed_tokens
        new_tokens.length.should == 1
        new_tokens.should == tokens
      end

      Time.travel(Time.now + Rack::MiniProfiler::AbstractStore::MAX_TOKEN_AGE + 1) do
        new_tokens = @store.allowed_tokens
        new_tokens.length.should == 2
        (new_tokens - tokens).length.should == 1
      end

    end
  end
  describe 'allowed_tokens' do
    before do
      @store = Rack::MiniProfiler::RedisStore.new(:db=>2)
    end

    it 'should return tokens' do

      @store.flush_tokens

      tokens = @store.allowed_tokens
      tokens.length.should == 1

      @store.simulate_expire

      new_tokens = @store.allowed_tokens

      new_tokens.length.should == 2
      (new_tokens - tokens).length.should == 1
    end
  end

      def travel(to)
        @now = to
        yield
      ensure
        @now = nil
      end