CVE-2016-0721 (Ruby, published 2017-04-21T15:59Z)
Description: value: Session fixation vulnerability in pcsd in pcs before 0.9.157.
Severity: MEDIUM (CVSS v3 base score: 8.1)
Weakness: CWE-384 \u2014 Session Fixation
Authenticating a user, or otherwise establishing a new user session, without invalidating any existing session identifier gives an attacker the opportunity to steal authenticated sessions.
Repository: https://github.com/ClusterLabs/pcs
Files changed:
pcsd/pcsd.conf
pcsd/pcsd.rb
pcsd/session.rb
pcsd/test/test_all_suite.rb
pcsd/test/test_session.rb
Commit message: web UI auth: store session data on server side


Vulnerable code:
RACK_ENV=production
use Rack::Session::Cookie,
  :expire_after => 60 * 60,
    session.clear

Fixed code:
# Set web UI sesions lifetime
SESSION_LIFETIME=3600
# Do not change
RACK_ENV=production
require 'session.rb'
session_lifetime = ENV['SESSION_LIFETIME'].to_i()
session_lifetime = 60 * 60 unless session_lifetime > 0
use SessionPoolLifetime,
  :expire_after => session_lifetime,
# session storage instance
# will be created by Rack later and fetched in "before" filter
$session_storage = nil
$session_storage_env = {}


  # get session storage instance from env
  if not $session_storage and env[:__session_storage]
    $session_storage = env[:__session_storage]
    $session_storage_env = env
  end

$thread_session_expired = Thread.new {
  while true
    sleep(60 * 5)
    begin
      if $session_storage
        $session_storage.drop_expired($session_storage_env)
      end
    rescue => e
      $logger.warn("Exception while removing expired sessions: #{e}")
    end
  end
}

    session.destroy
require 'rack/session/pool'

class SessionPoolLifetime < Rack::Session::Pool

  def initialize(app, options={})
    super
    @pool_timestamp = Hash.new()
  end

  def call(env)
    # save session storage to env so we can get it later
    env[:__session_storage] = self
    super
  end

  def get_session(env, sid)
    with_lock(env) do
      now = Time.now()
      # delete the session if expired
      if @default_options[:expire_after] and sid and @pool_timestamp[sid] and
        @pool_timestamp[sid] < (now - @default_options[:expire_after])
      then
        delete_session(sid)
      end
      # create new session if nonexistent
      unless sid and session = @pool[sid]
        sid, session = generate_sid, {}
        @pool.store sid, session
      end
      # bump session's access time
      @pool_timestamp[sid] = now
      [sid, session]
    end
  end

  def set_session(env, session_id, new_session, options)
    with_lock(env) do
      @pool.store session_id, new_session
      # bump session's access time
      @pool_timestamp[session_id] = Time.now()
      session_id
    end
  end

  def destroy_session(env, session_id, options)
    with_lock(env) do
      delete_session(session_id)
      generate_sid unless options[:drop]
    end
  end

  def drop_expired(env)
    return unless lifetime = @default_options[:expire_after]
    with_lock(env) {
      threshold = Time.now() - lifetime
      @pool_timestamp.select { |sid, timestamp|
        timestamp < threshold
      }.keys.each { |sid|
        delete_session(sid)
      }
    }
  end

  private

  def delete_session(sid)
    @pool.delete(sid)
    @pool_timestamp.delete(sid)
  end
end

require 'test_session.rb'
require 'test/unit'

require 'pcsd_test_utils.rb'
require 'session.rb'

class TestSessionPool < Test::Unit::TestCase

  def setup()
    @env = {
      'rack.multithread' => true,
    }
  end

  def fixture_get_pool(lifetime)
    pool = SessionPoolLifetime.new(nil, {:expire_after => lifetime,})
    (1..3).each { |i| pool.set_session(@env, "sid#{i}", {'value' => i}, {}) }
    return pool
  end

  def test_drop_expired_on_get()
    lifetime = 2
    pool = fixture_get_pool(lifetime)
    # touch sessions each second
    lifetime.times {
      sleep(1)
      assert_equal({'value' => 1}, pool.get_session(@env, 'sid1')[1])
      assert_equal({'value' => 3}, pool.get_session(@env, 'sid3')[1])
    }
    # after @lifetime passes the unused session gets removed on access
    sleep(1)
    assert_equal({'value' => 1}, pool.get_session(@env, 'sid1')[1])
    assert_equal({'value' => 3}, pool.get_session(@env, 'sid3')[1])
    assert_equal({}, pool.get_session(@env, 'sid2')[1])
  end

  def test_drop_expired_explicit()
    lifetime = 2
    pool = fixture_get_pool(lifetime)
    # touch sessions each second (otherwise they will be removed on access)
    lifetime.times {
      sleep(1)
      pool.get_session(@env, 'sid2')
      pool.set_session(@env, 'sid3', {'value' => 33}, {})
    }
    sleep(1)

    pool.drop_expired(@env)
    assert_equal(
      {
        'sid2' => {'value' => 2,},
        'sid3' => {'value' => 33,},
      },
      pool.pool
    )
  end

  def test_no_lifetime()
    pool = fixture_get_pool(nil)
    sleep(1)
    assert_equal({'value' => 1}, pool.get_session(@env, 'sid1')[1])
    assert_equal({'value' => 2}, pool.get_session(@env, 'sid2')[1])
    assert_equal({'value' => 3}, pool.get_session(@env, 'sid3')[1])
    sleep(1)
    pool.drop_expired(@env)
    assert_equal({'value' => 1}, pool.get_session(@env, 'sid1')[1])
    assert_equal({'value' => 2}, pool.get_session(@env, 'sid2')[1])
    assert_equal({'value' => 3}, pool.get_session(@env, 'sid3')[1])
  end

end