Expire sessions after 30 min idle and 12 h absolute
Sessions were only bounded by a process restart or the LRU eviction, so a stolen or fixed session id could stay valid for months. A cookie TTL would not help, since an attacker with a copy of the id ignores it. The store entry now carries _created_at and _last_seen (monotonic time). They are kept out of the session dict as attributes of the Session object, so is_empty() and the app code are unaffected. open_session drops expired entries and returns a fresh, empty session. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ca023e705c
commit
cc3d064e64
1 file changed
+42
-4
+42
-4
@@ -1,3 +1,5 @@
|
||||
import time
|
||||
|
||||
from flask import request
|
||||
from flask.sessions import SessionInterface as FlaskSessionInterface, SessionMixin
|
||||
from itsdangerous import BadSignature, Signer, want_bytes
|
||||
@@ -5,6 +7,14 @@ from werkzeug.datastructures import CallbackDict
|
||||
|
||||
from max.crypto import get_random_string
|
||||
|
||||
# in seconds
|
||||
SESSION_IDLE_TIMEOUT = 30 * 60
|
||||
SESSION_ABSOLUTE_TIMEOUT = 12 * 60 * 60
|
||||
|
||||
# Keys for the session metadata in the session storage
|
||||
CREATED_AT_KEY = "_created_at"
|
||||
LAST_SEEN_KEY = "_last_seen"
|
||||
|
||||
|
||||
class SessionStorage:
|
||||
def __init__(self, maxcap=100):
|
||||
@@ -54,23 +64,33 @@ session_storage = SessionStorage()
|
||||
|
||||
|
||||
class Session(CallbackDict, SessionMixin):
|
||||
def __init__(self, id=None, data=None):
|
||||
def __init__(self, id=None, data=None, created_at=None, last_seen=None):
|
||||
def on_update(self):
|
||||
self.modified = True
|
||||
if not self.id:
|
||||
self.set_new_id()
|
||||
|
||||
self.id = id
|
||||
self.created_at = created_at
|
||||
self.last_seen = last_seen
|
||||
self.modified = False
|
||||
CallbackDict.__init__(self, data, on_update)
|
||||
|
||||
def is_empty(self):
|
||||
return len(self) == 0
|
||||
|
||||
def is_expired(self, now):
|
||||
return (
|
||||
now - self.created_at > SESSION_ABSOLUTE_TIMEOUT
|
||||
or now - self.last_seen > SESSION_IDLE_TIMEOUT
|
||||
)
|
||||
|
||||
def clear(self):
|
||||
"""Removes all data, including the entry in the session storage"""
|
||||
del session_storage[self.id]
|
||||
self.id = None
|
||||
self.created_at = None
|
||||
self.last_seen = None
|
||||
super().clear()
|
||||
|
||||
def set_new_id(self):
|
||||
@@ -87,7 +107,14 @@ class SessionInterface(FlaskSessionInterface):
|
||||
return app.config["SESSION_COOKIE_NAME"]
|
||||
|
||||
def make_session_object(self, session_id=None, data=None):
|
||||
return Session(session_id, data=data)
|
||||
created_at = last_seen = None
|
||||
if data is not None:
|
||||
data = dict(data) # do not modify the entry in the session storage
|
||||
created_at = data.pop(CREATED_AT_KEY)
|
||||
last_seen = data.pop(LAST_SEEN_KEY)
|
||||
return Session(
|
||||
session_id, data=data, created_at=created_at, last_seen=last_seen
|
||||
)
|
||||
|
||||
def open_session(self, app, request):
|
||||
session_id = request.cookies.get(self.get_cookie_name(app))
|
||||
@@ -107,7 +134,11 @@ class SessionInterface(FlaskSessionInterface):
|
||||
session_id = session_id.decode("utf-8", "strict")
|
||||
|
||||
data = session_storage[session_id]
|
||||
return self.make_session_object(session_id, data=data)
|
||||
session = self.make_session_object(session_id, data=data)
|
||||
if data is not None and session.is_expired(time.monotonic()):
|
||||
del session_storage[session_id]
|
||||
return self.make_session_object()
|
||||
return session
|
||||
|
||||
def save_session(self, app, session, response):
|
||||
domain = self.get_cookie_domain(app)
|
||||
@@ -123,7 +154,14 @@ class SessionInterface(FlaskSessionInterface):
|
||||
httponly = self.get_cookie_httponly(app)
|
||||
secure = self.get_cookie_secure(app)
|
||||
expires = self.get_expiration_time(app, session)
|
||||
session_storage[session.id] = dict(session)
|
||||
session.last_seen = time.monotonic()
|
||||
if session.created_at is None:
|
||||
session.created_at = session.last_seen
|
||||
session_storage[session.id] = {
|
||||
**session,
|
||||
CREATED_AT_KEY: session.created_at,
|
||||
LAST_SEEN_KEY: session.last_seen,
|
||||
}
|
||||
session_id = (
|
||||
self._get_signer(app).sign(want_bytes(session.id)).decode("utf-8")
|
||||
)
|
||||
|
||||
Reference in new issue
Block a user