From 49bb7f1369f4a60fe3fd73c87d388c3d4db67f36 Mon Sep 17 00:00:00 2001 From: Darsh Kelaiya Date: Wed, 5 Aug 2026 22:37:54 -0700 Subject: [PATCH] python3-flask: fix CVE-2026-27205 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 Signed-off-by: Anuj Mittal --- .../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 --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 +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 +--- + 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"