Version in base suite: 3.1.1-1 Base version: flask_3.1.1-1 Target version: flask_3.1.1-1+deb13u1 Base file: /srv/ftp-master.debian.org/ftp/pool/main/f/flask/flask_3.1.1-1.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/f/flask/flask_3.1.1-1+deb13u1.dsc changelog | 7 patches/CVE-2026-27205-request-context-tracks-session-access.patch | 256 ++++++++++ patches/series | 1 3 files changed, 264 insertions(+) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmp8u74ymq2/flask_3.1.1-1.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmp8u74ymq2/flask_3.1.1-1+deb13u1.dsc: no acceptable signature found diff -Nru flask-3.1.1/debian/changelog flask-3.1.1/debian/changelog --- flask-3.1.1/debian/changelog 2025-05-17 08:15:33.000000000 +0000 +++ flask-3.1.1/debian/changelog 2026-07-06 01:34:06.000000000 +0000 @@ -1,3 +1,10 @@ +flask (3.1.1-1+deb13u1) trixie; urgency=medium + + * Team upload + * d/patches: backport upstream fix for CVE-2026-27205 (Closes: #1128620) + + -- Christopher Obbard Mon, 06 Jul 2026 02:34:06 +0100 + flask (3.1.1-1) unstable; urgency=medium * Team upload diff -Nru flask-3.1.1/debian/patches/CVE-2026-27205-request-context-tracks-session-access.patch flask-3.1.1/debian/patches/CVE-2026-27205-request-context-tracks-session-access.patch --- flask-3.1.1/debian/patches/CVE-2026-27205-request-context-tracks-session-access.patch 1970-01-01 00:00:00.000000000 +0000 +++ flask-3.1.1/debian/patches/CVE-2026-27205-request-context-tracks-session-access.patch 2026-07-06 01:34:06.000000000 +0000 @@ -0,0 +1,256 @@ +From: David Lord +Date: Wed, 18 Feb 2026 19:02:54 -0800 +Subject: CVE-2026-27205 request context tracks session access + +Backport upstream fix to ensure request contexts correctly track +session access. + +This fixes CVE-2026-27205. + +Origin: backport, https://github.com/pallets/flask/commit/c17f379390731543eea33a570a47bd4ef76a54fa +Forwarded: not-needed +Bug-Debian: https://bugs.debian.org/1128620 +Last-Update: 2026-07-06 +Signed-off-by: Christopher Obbard +--- + 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 +++++++++++++++++++++++++++++++++---------------- + 5 files changed, 71 insertions(+), 53 deletions(-) + +diff --git a/src/flask/app.py b/src/flask/app.py +index 1232b03..43d56f8 100644 +--- a/src/flask/app.py ++++ b/src/flask/app.py +@@ -1318,8 +1318,8 @@ class Flask(App): + 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 + +diff --git a/src/flask/ctx.py b/src/flask/ctx.py +index 9b164d3..cf285bc 100644 +--- a/src/flask/ctx.py ++++ b/src/flask/ctx.py +@@ -324,7 +324,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. +@@ -351,7 +351,7 @@ class RequestContext: + self.app, + environ=self.request.environ, + request=self.request, +- session=self.session, ++ session=self._session, + ) + + def match_request(self) -> None: +@@ -364,6 +364,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. +@@ -381,12 +391,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. +diff --git a/src/flask/sessions.py b/src/flask/sessions.py +index 4ffde71..e8ddca4 100644 +--- a/src/flask/sessions.py ++++ b/src/flask/sessions.py +@@ -43,10 +43,15 @@ class SessionMixin(MutableMapping[str, t.Any]): + #: ``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[str, t.Any], SessionMixin): +@@ -65,34 +70,15 @@ class SecureCookieSession(CallbackDict[str, t.Any], SessionMixin): + #: 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: c.Mapping[str, t.Any] | c.Iterable[tuple[str, t.Any]] | None = None, ++ initial: c.Mapping[str, t.Any] | None = None, + ) -> None: + def on_update(self: te.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 +diff --git a/src/flask/templating.py b/src/flask/templating.py +index 618a3b3..5f7480d 100644 +--- a/src/flask/templating.py ++++ b/src/flask/templating.py +@@ -22,8 +22,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) +@@ -32,7 +32,8 @@ def _default_template_ctx_processor() -> dict[str, t.Any]: + 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 + + +diff --git a/tests/test_basic.py b/tests/test_basic.py +index c372a91..4b3374e 100644 +--- a/tests/test_basic.py ++++ b/tests/test_basic.py +@@ -20,6 +20,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( + python_implementation() != "CPython", +@@ -231,27 +233,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): diff -Nru flask-3.1.1/debian/patches/series flask-3.1.1/debian/patches/series --- flask-3.1.1/debian/patches/series 2025-05-17 07:08:02.000000000 +0000 +++ flask-3.1.1/debian/patches/series 2026-07-06 01:34:06.000000000 +0000 @@ -2,3 +2,4 @@ PATCH-Make-the-documentation-build-reproducibly.patch Remove-eticalads-in-doc.patch docs-Use-intersphix-with-Debian-packages.patch +CVE-2026-27205-request-context-tracks-session-access.patch