mirror of
https://github.com/openembedded/meta-openembedded.git
synced 2026-09-02 04:20:18 +00:00
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> Signed-off-by: Anuj Mittal <anuj.mittal@oss.qualcomm.com>
261 lines
9.6 KiB
Diff
261 lines
9.6 KiB
Diff
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
|
|
|