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 <noreply@anthropic.com>
This commit is contained in:
finnandClaude Opus 5.5 committed 2026-10-21 17:40:54 +00:00
1 parent 8e32727fcd
commit 9aed6b5227
2 files changed
+16 -13

No files matched your search

+2 -1
View File
@@ -47,8 +47,9 @@ def main():
SECRET_KEY=( SECRET_KEY=(
"123456" if app.debug else base64.b64encode(os.urandom(64)).decode("utf-8") "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_SECURE=not app.debug,
SESSION_COOKIE_SAMESITE=False, SESSION_COOKIE_SAMESITE="Lax",
) )
add_middleware_stack( add_middleware_stack(
app, app,
+14 -12
View File
@@ -139,19 +139,27 @@ class SessionInterface(FlaskSessionInterface):
return self.make_session_object() return self.make_session_object()
return session 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): 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_name = self.get_cookie_name(app)
cookie_attributes = self.get_cookie_attributes(app)
if session.is_empty(): if session.is_empty():
if session.modified: if session.modified:
del session_storage[session.id] del session_storage[session.id]
# Only send the deletion header if the client sent a cookie # Only send the deletion header if the client sent a cookie
if cookie_name in request.cookies: if cookie_name in request.cookies:
response.delete_cookie(cookie_name, domain=domain, path=path) response.delete_cookie(cookie_name, **cookie_attributes)
else: else:
httponly = self.get_cookie_httponly(app)
secure = self.get_cookie_secure(app)
expires = self.get_expiration_time(app, session) expires = self.get_expiration_time(app, session)
session.last_seen = time.monotonic() session.last_seen = time.monotonic()
if session.created_at is None: 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") self._get_signer(app).sign(want_bytes(session.id)).decode("utf-8")
) )
response.set_cookie( response.set_cookie(
cookie_name, cookie_name, session_id, expires=expires, **cookie_attributes
session_id,
expires=expires,
httponly=httponly,
domain=domain,
path=path,
secure=secure,
) )