Package: release.debian.org
Severity: normal
Tags: trixie
X-Debbugs-Cc: [email protected], [email protected]
Control: affects -1 + src:flask
User: [email protected]
Usertags: pu

[ Reason ]
This update fixes a minor security issue in Flask where request context
handling did not correctly track session access in some cases.

The fix is a targeted backport of the upstream security patch:

https://github.com/pallets/flask/commit/c17f379390731543eea33a570a47bd4ef76a54fa

This fixes CVE-2026-27205.

This issue affects the version currently in trixie. The proposed update
backports the upstream fix with no unrelated changes.

[ Impact ]
If the update is not approved users of Flask in trixie remain affected
by the security issue. Applications relying on Flask session handling may
continue to have incorrect request/session access tracking behaviour.

[ Tests ]
The patch includes the corresponding upstream test coverage for the
changed behaviour. The package builds successfully with the patch applied
and the relevant test suite is ran during package build. Also I manually
verified the patch behaviour.

[ Risks ]
The risk is low - the change is a targeted backport from upstream and is
limited to request/session context tracking. The patch is small, includes
regression test coverage and does not introduce new dependencies or
packaging changes.

The affected code is central Flask request-context handling so there is
some theoretical regression risk for applications using sessions but the
patch matches the upstream fix and is limited in scope.

[ Checklist ]
  [x] *all* changes are documented in the d/changelog
  [x] I reviewed all changes and I approve them
  [x] attach debdiff against the package in (old)stable
  [x] the issue is verified as fixed in unstable

[ Changes ]
The update backports the upstream fix for request context session-access
tracking.

Packaging changes:
- Add a Debian patch backporting the upstream fix.
- Add the corresponding changelog entry for 3.1.1-1+deb13u1.

[ Other info ]
n/a
diff -Nru flask-3.1.1/debian/changelog flask-3.1.1/debian/changelog
--- flask-3.1.1/debian/changelog        2025-05-17 09:15:33.000000000 +0100
+++ flask-3.1.1/debian/changelog        2026-07-06 02:34:06.000000000 +0100
@@ -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 <[email protected]>  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 01:00:00.000000000 +0100
+++ 
flask-3.1.1/debian/patches/CVE-2026-27205-request-context-tracks-session-access.patch
       2026-07-06 02:34:06.000000000 +0100
@@ -0,0 +1,256 @@
+From: David Lord <[email protected]>
+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 <[email protected]>
+---
+ 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 08:08:02.000000000 +0100
+++ flask-3.1.1/debian/patches/series   2026-07-06 02:34:06.000000000 +0100
@@ -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

Reply via email to