Skip to content

fix(session): keep responses that carry a session token out of shared caches - #303

Merged
allen0099 merged 2 commits into
masterfrom
fix/session-token-uncacheable
Sep 27, 2026
Merged

allen0099 merged 2 commits into
masterfrom
fix/session-token-uncacheable

Conversation

@allen0099

@allen0099 allen0099 commented Sep 27, 2026 •

Copy link
Copy Markdown
Owner

Problem

When FastAPICacheXSessionMiddleware sends a session token to the client (a new session, a sliding renewal, a regenerated ID) via Set-Cookie or the X-Session-Token header, nothing stops a shared cache from storing it. Vary was only added when the handler accessed request.session, and Cache-Control was left as the route set it. On a @cache(ttl=60, public=True) route, a request that triggered sliding renewal got Cache-Control: public, max-age=60, no Vary, and Set-Cookie: session=<valid token>, so a CDN or reverse proxy could hand that token to the next visitors. #168 fixed which header names go in Vary, but it only covered responses where the session was accessed.

Fix

fastapi_cachex/session/middleware.py:

  • _persist now returns whether it wrote a token or a clearing cookie. It covers every _emit_token call and both clear-cookie appends.
  • When it did, send_wrapper sets Cache-Control: private, no-store, replacing any existing value, and adds Vary for the transport headers consulted (vary_on: the token header, Authorization when bearer tokens are enabled, and Cookie for cookie transport), whether or not the session was accessed.
  • The deprecated SessionMiddleware.dispatch does the same when it sets the token response header.
  • A new _add_vary helper skips names that are already present (compared case-insensitively) and leaves Vary: * alone. Starlette's MutableHeaders.add_vary_header appends without checking, so a route that already sent Vary: Cookie would otherwise get Cookie twice. The existing session.accessed path uses the helper too.

Responses that carry no token keep their headers unchanged, so a @cache(public=True) response that doesn't renew or create a session is still cacheable.

The behaviour is documented in docs/SESSION.md and i18n/zh-TW/docs/SESSION.md (in the request.session section, next to the existing Vary bullet).

Tests

New file tests/session/test_token_response_caching.py (15 tests):

  • new session cookie
  • sliding renewal on a @cache(public=True) route over cookie, header and bearer transport
  • regenerated ID over cookie and header
  • logout clearing cookie
  • clearing cookie for an emptied anonymous session
  • deprecated SessionMiddleware renewal
  • existing Vary values kept and not duplicated
  • Vary: * left as it is
  • no-token responses keep public, max-age=60 and get no Vary: a loaded session outside the renewal window, no session at all, the deprecated middleware without renewal, and an emptied anonymous header session

Mutation check: with middleware.py reverted to master, 11 of the new tests fail. The 4 that pass are the no-token guards, and they are expected to pass on master too. Targeted mutations:

  • Vary only when accessed: 6 fail
  • no Cache-Control override: 11 fail
  • no de-duplication: 1 fails
  • no * check: 1 fails
  • no Vary in the deprecated middleware: 1 fails

Full suite: 872 passed, 191 skipped (live Redis/Memcached). Coverage 93.71%; session/middleware.py is at 100%. ruff check, ruff format --check, mypy --strict and pre-commit all pass.

CHANGELOG

The entry is in changelog.d/297.security.md and is merged into CHANGELOG.md at release time.

Closes #297

… caches

When FastAPICacheXSessionMiddleware sent a token (new session, sliding
renewal, regenerated ID) or a clearing cookie, nothing stopped a CDN or
reverse proxy from storing it: Vary was only added when the handler
accessed request.session and Cache-Control was left as the route set it,
so a @cache(public=True) route could hand a valid session cookie to the
next visitor.

Such responses now get Cache-Control: private, no-store (replacing any
existing value) and Vary on the transport headers, whatever the handler
did with the session. The deprecated SessionMiddleware does the same when
it sends a token. Vary names already present are no longer duplicated.

Closes #297
@allen0099 allen0099 added this to the 0.3.9 milestone Sep 27, 2026
@allen0099 allen0099 added bug Something isn't working session Session management subsystem security Security vulnerability or hardening labels Sep 27, 2026
@allen0099
allen0099 merged commit a0e7e2c into master Sep 27, 2026
12 checks passed
@allen0099
allen0099 deleted the fix/session-token-uncacheable branch September 27, 2026 13:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working security Security vulnerability or hardening session Session management subsystem

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Session middleware: responses carrying a session token can be stored by shared caches

1 participant