File CVE-2026-27205.patch of Package python-Flask.43036
From c17f379390731543eea33a570a47bd4ef76a54fa Mon Sep 17 00:00:00 2001
From: David Lord <davidism@gmail.com>
Date: Wed, 18 Feb 2026 19:02:54 -0800
Subject: [PATCH] request context tracks session access
---
CHANGES.rst | 3 +++
src/flask/app.py | 4 +--
src/flask/ctx.py | 22 +++++++++++-----
src/flask/sessions.py | 34 ++++++++----------------
src/flask/templating.py | 7 ++---
tests/test_basic.py | 57 ++++++++++++++++++++++++++++-------------
6 files changed, 74 insertions(+), 53 deletions(-)
Index: Flask-2.3.2/src/flask/app.py
===================================================================
--- Flask-2.3.2.orig/src/flask/app.py
+++ Flask-2.3.2/src/flask/app.py
@@ -2001,8 +2001,8 @@ class Flask(Scaffold):
for func in reversed(self.after_request_funcs[name]):
response = self.ensure_sync(func)(response)
- if not self.session_interface.is_null_session(ctx.session):
- self.session_interface.save_session(self, ctx.session, response)
+ if not self.session_interface.is_null_session(ctx._session):
+ self.session_interface.save_session(self, ctx._session, response)
return response
Index: Flask-2.3.2/src/flask/ctx.py
===================================================================
--- Flask-2.3.2.orig/src/flask/ctx.py
+++ Flask-2.3.2/src/flask/ctx.py
@@ -317,7 +317,7 @@ class RequestContext:
except HTTPException as e:
self.request.routing_exception = e
self.flashes: list[tuple[str, str]] | None = None
- self.session: SessionMixin | None = session
+ self._session: SessionMixin | None = session
# Functions that should be executed after the request on the response
# object. These will be called before the regular "after_request"
# functions.
@@ -342,7 +342,7 @@ class RequestContext:
self.app,
environ=self.request.environ,
request=self.request,
- session=self.session,
+ session=self._session,
)
def match_request(self) -> None:
@@ -355,6 +355,16 @@ class RequestContext:
except HTTPException as e:
self.request.routing_exception = e
+ @property
+ def session(self) -> SessionMixin:
+ """The session data associated with this request. Not available until
+ this context has been pushed. Accessing this property, also accessed by
+ the :data:`~flask.session` proxy, sets :attr:`.SessionMixin.accessed`.
+ """
+ assert self._session is not None, "The session has not yet been opened."
+ self._session.accessed = True
+ return self._session
+
def push(self) -> None:
# Before we push the request context we have to ensure that there
# is an application context.
@@ -372,12 +382,12 @@ class RequestContext:
# This allows a custom open_session method to use the request context.
# Only open a new session if this is the first time the request was
# pushed, otherwise stream_with_context loses the session.
- if self.session is None:
+ if self._session is None:
session_interface = self.app.session_interface
- self.session = session_interface.open_session(self.app, self.request)
+ self._session = session_interface.open_session(self.app, self.request)
- if self.session is None:
- self.session = session_interface.make_null_session(self.app)
+ if self._session is None:
+ self._session = session_interface.make_null_session(self.app)
# Match the request URL after loading the session, so that the
# session is available in custom URL converters.
Index: Flask-2.3.2/src/flask/sessions.py
===================================================================
--- Flask-2.3.2.orig/src/flask/sessions.py
+++ Flask-2.3.2/src/flask/sessions.py
@@ -39,10 +39,15 @@ class SessionMixin(MutableMapping):
#: ``True``.
modified = True
- #: Some implementations can detect when session data is read or
- #: written and set this when that happens. The mixin default is hard
- #: coded to ``True``.
- accessed = True
+ accessed = False
+ """Indicates if the session was accessed, even if it was not modified. This
+ is set when the session object is accessed through the request context,
+ including the global :data:`.session` proxy. A ``Vary: cookie`` header will
+ be added if this is ``True``.
+
+ .. versionchanged:: 3.1.3
+ This is tracked by the request context.
+ """
class SecureCookieSession(CallbackDict, SessionMixin):
@@ -61,32 +66,12 @@ class SecureCookieSession(CallbackDict,
#: will only be written to the response if this is ``True``.
modified = False
- #: When data is read or written, this is set to ``True``. Used by
- # :class:`.SecureCookieSessionInterface` to add a ``Vary: Cookie``
- #: header, which allows caching proxies to cache different pages for
- #: different users.
- accessed = False
-
def __init__(self, initial: t.Any = None) -> None:
def on_update(self) -> None:
self.modified = True
- self.accessed = True
super().__init__(initial, on_update)
- def __getitem__(self, key: str) -> t.Any:
- self.accessed = True
- return super().__getitem__(key)
-
- def get(self, key: str, default: t.Any = None) -> t.Any:
- self.accessed = True
- return super().get(key, default)
-
- def setdefault(self, key: str, default: t.Any = None) -> t.Any:
- self.accessed = True
- return super().setdefault(key, default)
-
-
class NullSession(SecureCookieSession):
"""Class used to generate nicer error messages if sessions are not
available. Will still allow read-only access to the empty session
Index: Flask-2.3.2/src/flask/templating.py
===================================================================
--- Flask-2.3.2.orig/src/flask/templating.py
+++ Flask-2.3.2/src/flask/templating.py
@@ -21,8 +21,8 @@ if t.TYPE_CHECKING: # pragma: no cover
def _default_template_ctx_processor() -> dict[str, t.Any]:
- """Default template context processor. Injects `request`,
- `session` and `g`.
+ """Default template context processor. Replaces the ``request`` and ``g``
+ proxies with their concrete objects for faster access.
"""
appctx = _cv_app.get(None)
reqctx = _cv_request.get(None)
@@ -31,7 +31,8 @@ def _default_template_ctx_processor() ->
rv["g"] = appctx.g
if reqctx is not None:
rv["request"] = reqctx.request
- rv["session"] = reqctx.session
+ # The session proxy cannot be replaced, accessing it gets
+ # RequestContext.session, which sets session.accessed.
return rv
Index: Flask-2.3.2/tests/test_basic.py
===================================================================
--- Flask-2.3.2.orig/tests/test_basic.py
+++ Flask-2.3.2/tests/test_basic.py
@@ -18,6 +18,8 @@ from werkzeug.routing import BuildError
from werkzeug.routing import RequestRedirect
import flask
+from flask.globals import request_ctx
+from flask.testing import FlaskClient
require_cpython_gc = pytest.mark.skipif(
@@ -228,27 +230,46 @@ def test_endpoint_decorator(app, client)
assert client.get("/foo/bar").data == b"bar"
-def test_session(app, client):
- @app.route("/set", methods=["POST"])
- def set():
- assert not flask.session.accessed
- assert not flask.session.modified
+def test_session_accessed(app: flask.Flask, client: FlaskClient) -> None:
+ @app.post("/")
+ def do_set():
flask.session["value"] = flask.request.form["value"]
- assert flask.session.accessed
- assert flask.session.modified
return "value set"
- @app.route("/get")
- def get():
- assert not flask.session.accessed
- assert not flask.session.modified
- v = flask.session.get("value", "None")
- assert flask.session.accessed
- assert not flask.session.modified
- return v
-
- assert client.post("/set", data={"value": "42"}).data == b"value set"
- assert client.get("/get").data == b"42"
+ @app.get("/")
+ def do_get():
+ return flask.session.get("value", "None")
+
+ @app.get("/nothing")
+ def do_nothing() -> str:
+ return ""
+
+ with client:
+ rv = client.get("/nothing")
+ assert "cookie" not in rv.vary
+ assert not request_ctx._session.accessed
+ assert not request_ctx._session.modified
+
+ with client:
+ rv = client.post(data={"value": "42"})
+ assert rv.text == "value set"
+ assert "cookie" in rv.vary
+ assert request_ctx._session.accessed
+ assert request_ctx._session.modified
+
+ with client:
+ rv = client.get()
+ assert rv.text == "42"
+ assert "cookie" in rv.vary
+ assert request_ctx._session.accessed
+ assert not request_ctx._session.modified
+
+ with client:
+ rv = client.get("/nothing")
+ assert rv.text == ""
+ assert "cookie" not in rv.vary
+ assert not request_ctx._session.accessed
+ assert not request_ctx._session.modified
def test_session_path(app, client):