From 9aed6b52279fd30ed9aa9e5ab211f8311f7b7291 Mon Sep 17 00:00:00 2001 From: Finn Stutzenstein Date: Wed, 21 Oct 2026 17:40:54 +0000 Subject: [PATCH] Set SameSite=Lax and use the __Host- prefix for the session cookie The own SessionInterface ignored SESSION_COOKIE_SAMESITE, so the cookie was sent with every cross-site request in Firefox and Safari (CSRF, clickjacking in iframes, keeping the idle timeout alive). The cookie name "session" could also be set by other subdomains or via plain HTTP, the precondition for session fixation. - SameSite=Lax. No GET changes state, so links from mails keep working. - In production the cookie is named __Host-session, which browsers only accept with Secure, Path=/ and without Domain. The debug server keeps "session", since it runs without Secure. - set_cookie and delete_cookie use the same attributes, since browsers ignore the deletion of a __Host- cookie without Secure. Co-Authored-By: Claude Opus 5.5 --- max/app.py | 3 ++- max/sessions.py | 26 ++++++++++++++------------ 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/max/app.py b/max/app.py index 24ae8b7..f59464e 100644 --- a/max/app.py +++ b/max/app.py @@ -47,8 +47,9 @@ def main(): SECRET_KEY=( "123456" if app.debug else base64.b64encode(os.urandom(64)).decode("utf-8") ), + SESSION_COOKIE_NAME="session" if app.debug else "__Host-session", SESSION_COOKIE_SECURE=not app.debug, - SESSION_COOKIE_SAMESITE=False, + SESSION_COOKIE_SAMESITE="Lax", ) add_middleware_stack( app, diff --git a/max/sessions.py b/max/sessions.py index ac30b1d..1e02157 100644 --- a/max/sessions.py +++ b/max/sessions.py @@ -139,19 +139,27 @@ class SessionInterface(FlaskSessionInterface): return self.make_session_object() return session + def get_cookie_attributes(self, app): + # Attributes for setting and deleting the cookie must match, because + # browsers ignore a deletion of a __Host- cookie without Secure and Path. + return dict( + domain=self.get_cookie_domain(app), + path=self.get_cookie_path(app), + secure=self.get_cookie_secure(app), + httponly=self.get_cookie_httponly(app), + samesite=self.get_cookie_samesite(app), + ) + def save_session(self, app, session, response): - domain = self.get_cookie_domain(app) - path = self.get_cookie_path(app) cookie_name = self.get_cookie_name(app) + cookie_attributes = self.get_cookie_attributes(app) if session.is_empty(): if session.modified: del session_storage[session.id] # Only send the deletion header if the client sent a cookie if cookie_name in request.cookies: - response.delete_cookie(cookie_name, domain=domain, path=path) + response.delete_cookie(cookie_name, **cookie_attributes) else: - httponly = self.get_cookie_httponly(app) - secure = self.get_cookie_secure(app) expires = self.get_expiration_time(app, session) session.last_seen = time.monotonic() if session.created_at is None: @@ -170,11 +178,5 @@ class SessionInterface(FlaskSessionInterface): self._get_signer(app).sign(want_bytes(session.id)).decode("utf-8") ) response.set_cookie( - cookie_name, - session_id, - expires=expires, - httponly=httponly, - domain=domain, - path=path, - secure=secure, + cookie_name, session_id, expires=expires, **cookie_attributes )