A post recently hit the Full Disclosure seclist titled “Move away from CookieStore if you care about your users and their security”. The post discusses a property of session cookies - notably, that hitting “logout” doesn’t prevent a cookie from being reused to regain that session later, since if someone manages to jack one of your users’ cookies, they can just replay that cookie again at any time and gain access to the users’ account.
It’s worth first noting that this vulnerability requires your user’s session cookies to be compromised in the first place, so the whole vulnerability hinges on with “if your user is already owned, then…”
That said, yes, if you use sessions naively, then a compromised cookie may be used to gain access to a user’s account so long as the application’s session secret hasn’t changed (thereby invalidating the cookie signature). Note that this is true for all session stores, though in the case of serverside sessions, this only holds until the session gets swept (which may happen on explicit logout, but does not necessarily happen at a defined time otherwise); presuming you have some kind of session TTL in play, an attacker could keep their jacked session ID active indefinitely there, as well. A hijacked session cookie is Bad News (which is why you should be using HTTPS and HTTPS-only cookies!) no matter how you slice it.
Fortunately, if you’re worried about this class of attack, mitigating it is Pretty Darn Simple.
If you just want parity with serverside session stores that just perform expired session sweeps, then you can enforce a TTL on a session by just providing a TTL value in the session, and validating that when the session is read, then updating it when the session is written. You could do this trivially with a Rack middleware, or if you just want it in your app:
class ApplicationController
before_filter :validate_session_timestamp
after_filter :persist_session_timestamp
SESSION_TTL = 48.hours
def validate_session_timestamp
if user? && session.key?(:ttl) && session[:ttl] < SESSION_TTL.ago
reset_session
current_user = nil
redirect_to login_path
end
end
def persist_session_timestamp
session[:ttl] = Time.now if user?
end
end
That’s it. Any session that hasn’t been touched in 48 hours won’t validate and will get tossed out, same as serverside sessions (and as a bonus, you don’t have to do any session sweeping yourself! Hooray!) This does leave the cookie vulerable to TTL refreshes, so perhaps you want something more robust.
Something that neither CookieStore or server-side stores can do by default is maintain a list of sessions associated with a given user, and provide the user a means to revoke access granted to previously-granted sessions. Consider the case where you walk away from a public computer having forgotten to hit “log out” - you have no means of invalidating that session from another computer. This is a problem!
Fortunately, it’s trivial enough to just save a list of active sessions if desired:
class User
# Presume an active_sessions field on the model that is large enough to hold some list of sessions:
serialize :active_sessions, Array
def activate_session(id)
active_sessions.push id unless active_sessions.include? id
save
end
def deactivate_session(id)
active_sessions.delete id
save
end
end
class SessionsController
def login
# ...
current_user.activate_session session[:session_id]
end
def logout
current_user.deactivate_session session[:session_id]
reset_session
# ...
end
end
class ApplicationController
before_filter :validate_active_session
def validate_active_session
if user? && !current_user.active_sessions.include? session[:session_id]
reset_session
redirect_to login_path
end
end
end
You could get more complex with this and save things like the last IP and geolocation that each session was active from, and present that to the user, GMail-style. You could enforce a maximum number of sessions that can be active at any given time. This makes it easy to let users log out other sessions:
class User
def expire_sessions!(active)
self.active_sessions = [active]
save
end
end
class UserController
def logout_other_sessions
current_user.expire_sessions! session[:session_id]
# Redirect or whatever
end
end
This technique is applicable to all session stores, not just CookieStore (you won’t be able to get a list of sessions for a given user ID by default in ActiveRecordStore or whatever other serverside store you might want to use). You can give your users proactive control over their account security, and keep using CookieStore with all its benefits (like being invulnerable to session fixation!)