diff mbox series

[meta-python,scarthgap] python3-flask: fix CVE-2026-27205

Message ID 20260806053754.2932456-1-dkelaiya@cisco.com
State New
Headers show
Series [meta-python,scarthgap] python3-flask: fix CVE-2026-27205 | expand

Commit Message

From: Darsh Kelaiya <dkelaiya@cisco.com>

This patch applies the upstream fix as referenced in [2], using the
commit shown in [1].

[1] https://github.com/pallets/flask/commit/c17f379390731543eea33a570a47bd4ef76a54fa
[2] https://github.com/pallets/flask/security/advisories/GHSA-68rp-wp8r-4726

Signed-off-by: Darsh Kelaiya <dkelaiya@cisco.com>
---
 .../python/python3-flask/CVE-2026-27205.patch | 260 ++++++++++++++++++
 .../python/python3-flask_3.0.3.bb             |   2 +
 2 files changed, 262 insertions(+)
 create mode 100644 meta-python/recipes-devtools/python/python3-flask/CVE-2026-27205.patch
diff mbox series

Patch

diff --git a/meta-python/recipes-devtools/python/python3-flask/CVE-2026-27205.patch b/meta-python/recipes-devtools/python/python3-flask/CVE-2026-27205.patch
new file mode 100644
index 0000000000..05d6552d2b
--- /dev/null
+++ b/meta-python/recipes-devtools/python/python3-flask/CVE-2026-27205.patch
@@ -0,0 +1,260 @@ 
+From 40ed97132b22a99bee3c8b7d819c2113a767aabe 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
+
+CVE: CVE-2026-27205
+Upstream-Status: Backport [https://github.com/pallets/flask/commit/c17f379390731543eea33a570a47bd4ef76a54fa]
+
+Backport Changes:
+- Omitted CHANGES.rst because the upstream advisory entry targets the
+  Flask 3.1.3 release section, which is absent from Scarthgap 3.0.3 and
+  is not required for the runtime fix.
+- Retained the Flask 3.0.3 SecureCookieSession.__init__() t.Any
+  annotation because upstream's collections.abc.Mapping typing layout
+  is not present in this version. The session-access behavior and all
+  regression tests are preserved unchanged.
+
+(cherry picked from commit c17f379390731543eea33a570a47bd4ef76a54fa)
+Signed-off-by: Darsh Kelaiya <dkelaiya@cisco.com>
+---
+ src/flask/app.py        |  4 +--
+ src/flask/ctx.py        | 22 +++++++++++-----
+ src/flask/sessions.py   | 32 +++++++----------------
+ src/flask/templating.py |  7 ++---
+ tests/test_basic.py     | 57 ++++++++++++++++++++++++++++-------------
+ 5 files changed, 70 insertions(+), 52 deletions(-)
+
+diff --git a/src/flask/app.py b/src/flask/app.py
+index 7622b5e8..d537ad80 100644
+--- a/src/flask/app.py
++++ b/src/flask/app.py
+@@ -1280,8 +1280,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 9b164d39..cf285bc0 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 ee19ad63..1e70ef7b 100644
+--- a/src/flask/sessions.py
++++ b/src/flask/sessions.py
+@@ -43,10 +43,15 @@ class SessionMixin(MutableMapping):  # type: ignore[type-arg]
+     #: ``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.
++    """
+ 
+ 
+ # TODO generic when Python > 3.8
+@@ -66,31 +71,12 @@ class SecureCookieSession(CallbackDict, SessionMixin):  # type: ignore[type-arg]
+     #: 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: 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 618a3b35..5f7480da 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 214cfee0..754fe589 100644
+--- a/tests/test_basic.py
++++ b/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(
+     python_implementation() != "CPython",
+@@ -229,27 +231,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):
+-- 
+2.44.4
+
diff --git a/meta-python/recipes-devtools/python/python3-flask_3.0.3.bb b/meta-python/recipes-devtools/python/python3-flask_3.0.3.bb
index b68946ba0c..6e9db196ae 100644
--- a/meta-python/recipes-devtools/python/python3-flask_3.0.3.bb
+++ b/meta-python/recipes-devtools/python/python3-flask_3.0.3.bb
@@ -23,3 +23,5 @@  RDEPENDS:${PN} = " \
     python3-profile \
     python3-werkzeug \
 "
+
+SRC_URI += "file://CVE-2026-27205.patch"