Track session access and modification in SessionMiddleware - #3166
Merged
Merged
Conversation
Replace the plain `dict` used for `scope["session"]` with a `Session` subclass that tracks `accessed` and `modified` flags. - Only send `Set-Cookie` when the session was actually modified, not on every response. This prevents race conditions with concurrent requests where a slow read-only response clobbers a newer cookie. - Add `Vary: Cookie` header when the session is accessed, so reverse proxies and CDNs don't serve cached responses with session-dependent content to the wrong user. - `pop` and `setdefault` conditionally set `modified` only when the session actually changes, following Django and Flask conventions. Closes #2019
The dict_keys/dict_values/dict_items concrete types are not importable in Python, so use typing.Any as the return type to satisfy mypy strict mode without type: ignore comments.
Kludex
force-pushed
the
track-session-access-modification
branch
4 times, most recently
from
March 1, 2026 18:12
078743c to
cd7d3de
Compare
Move access tracking from per-method overrides (keys/values/items) on the Session dict to the HTTPConnection.session property, matching Flask 3.1.3's approach. This avoids the dict_keys/dict_values/dict_items typing problem entirely since those types are not importable in Python.
Kludex
force-pushed
the
track-session-access-modification
branch
from
March 1, 2026 18:13
cd7d3de to
dce3495
Compare
SessionMiddleware
This was referenced Mar 1, 2026
3 tasks
This was referenced Aug 25, 2026
maelgui
added a commit
to maelgui/bagadmenru-v5
that referenced
this pull request
Sep 12, 2026
…skey limité à beta La création auto de passkey (conditional create post-login) réussissait côté OS mais n'était jamais enregistrée serveur : le challenge WebAuthn vit dans la session cookie, et SessionMiddleware (starlette < 1.0) ré-émettait le Set-Cookie sur chaque réponse dont la session était non vide. Pendant la rafale post-login, une réponse read-only portant un cookie périmé écrasait le challenge entre /preregister et /register. Credential orphelin dans le trousseau, 400 avalé par le catch silencieux. Backend : - starlette épinglé >= 1.6 : le middleware ne ré-émet le cookie que si la session a été MODIFIÉE (Kludex/starlette#3166) — les réponses read-only ne peuvent plus écraser un challenge fraîchement écrit, pour toutes les utilisations de session, pas seulement celle-ci - tests de régression qui épinglent ce comportement (échouent sous 0.46, vérifiés) pour qu'un downgrade ne réintroduise pas la course - credential invalide -> 400 (au lieu de 500) - le login mot de passe purge le auth_challenge résiduel du login conditionnel (hygiène : la session se vide, le cookie est supprimé) Frontend : - upgrade silencieux (conditional create) limité à beta et dev local via VITE_ENVIRONMENT, fail closed si absent — même patron que la debug bar - un échec d'enregistrement APRÈS création du credential par l'OS n'est plus avalé : console.error + Sentry (le mode de défaillance qui fabrique des orphelins) Tests : 203 unit + 4 migrations backend, 63 frontend, lint/mypy verts, client OpenAPI inchangé.
maelgui
added a commit
to maelgui/bagadmenru-v5
that referenced
this pull request
Sep 12, 2026
…skey limité à beta La création auto de passkey (conditional create post-login) réussissait côté OS mais n'était jamais enregistrée serveur : le challenge WebAuthn vit dans la session cookie, et SessionMiddleware (starlette < 1.0) ré-émettait le Set-Cookie sur chaque réponse dont la session était non vide. Pendant la rafale post-login, une réponse read-only portant un cookie périmé écrasait le challenge entre /preregister et /register. Credential orphelin dans le trousseau, 400 avalé par le catch silencieux. Backend : - starlette 0.46.2 -> 1.6.0 via poetry.lock (pas de pin pyproject : fastapi gère la contrainte et déclare >= 0.46 sans borne haute) : le middleware ne ré-émet le cookie que si la session a été MODIFIÉE (Kludex/starlette#3166) — les réponses read-only ne peuvent plus écraser un challenge fraîchement écrit, pour toutes les utilisations de session - tests de régression qui épinglent ce comportement (échouent sous 0.46, vérifiés) pour qu'un downgrade ne réintroduise pas la course - credential invalide -> 400 (au lieu de 500) - le login mot de passe purge le auth_challenge résiduel du login conditionnel (hygiène : la session se vide, le cookie est supprimé) Frontend : - upgrade silencieux (conditional create) limité à beta et dev local via VITE_ENVIRONMENT, fail closed si absent — même patron que la debug bar - un échec d'enregistrement APRÈS création du credential par l'OS n'est plus avalé : console.error + Sentry (le mode de défaillance qui fabrique des orphelins) Tests : 203 unit + 4 migrations backend, 63 frontend, lint/mypy verts, client OpenAPI inchangé.
maelgui
added a commit
to maelgui/bagadmenru-v5
that referenced
this pull request
Sep 12, 2026
…skey limité à beta La création auto de passkey (conditional create post-login) réussissait côté OS mais n'était jamais enregistrée serveur : le challenge WebAuthn vit dans la session cookie, et SessionMiddleware (starlette < 1.0) ré-émettait le Set-Cookie sur chaque réponse dont la session était non vide. Pendant la rafale post-login, une réponse read-only portant un cookie périmé écrasait le challenge entre /preregister et /register. Credential orphelin dans le trousseau, 400 avalé par le catch silencieux. Backend : - starlette 0.46.2 -> 1.6.0 via poetry.lock (pas de pin pyproject : fastapi gère la contrainte et déclare >= 0.46 sans borne haute) : le middleware ne ré-émet le cookie que si la session a été MODIFIÉE (Kludex/starlette#3166) — les réponses read-only ne peuvent plus écraser un challenge fraîchement écrit, pour toutes les utilisations de session - tests de régression qui épinglent ce comportement (échouent sous 0.46, vérifiés) pour qu'un downgrade ne réintroduise pas la course - credential invalide -> 400 (au lieu de 500) - le login mot de passe purge le auth_challenge résiduel du login conditionnel (hygiène : la session se vide, le cookie est supprimé) Frontend : - upgrade silencieux (conditional create) limité à beta et dev local via VITE_ENVIRONMENT, fail closed si absent — même patron que la debug bar - un échec d'enregistrement APRÈS création du credential par l'OS n'est plus avalé : console.error + Sentry (le mode de défaillance qui fabrique des orphelins) Tests : 203 unit + 4 migrations backend, 63 frontend, lint/mypy verts, client OpenAPI inchangé.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
SessionMiddlewarecurrently sendsSet-Cookieon every response when the session is non-empty, even if nothing changed. This causes race conditions with concurrent requests - a slow read-only response can clobber a newer session cookie (see #2019, Chrome bug, authlib#334).This PR replaces the plain
dictused forscope["session"]with aSessionsubclass that tracks two flags:accessed: set inHTTPConnection.sessionwhen the session is accessed, following Flask 3.1.3's approach of tracking at the property level rather than per-method overridesmodified: set on write operations (__setitem__,__delitem__,clear,update), withpopandsetdefaultconditionally marking modified only when the session actually changes (following Django and Flask/Werkzeug conventions)The middleware then uses these flags to:
Set-CookiewhenmodifiedisTrueVary: CookiewhenaccessedisTrue, so reverse proxies and CDNs don't cache session-dependent responses incorrectlymodifiedtracking but bundles unrelated features (partitionedcookies,refresh_window). This PR scopes the change to just access/modification tracking.