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):
openSUSE Build Service is sponsored by