From 640ff67e9d59d7acd786683bbab422d8aa17211c Mon Sep 17 00:00:00 2001 From: "Brian C. Lane" Date: Tue, 9 May 2023 15:36:11 -0700 Subject: [PATCH 5/5] Backport support for the accessed attribute This is added to the SessionMixin and SecureCookieSession to support fixing CVE-2023-30861. Related: rhbz#2196683 --- flask/sessions.py | 40 +++++++++++++++++++++++++++++++++++++++- tests/test_basic.py | 8 ++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/flask/sessions.py b/flask/sessions.py index 2f60e7eb..278c3583 100644 --- a/flask/sessions.py +++ b/flask/sessions.py @@ -48,6 +48,11 @@ class SessionMixin(object): #: The default mixin implementation just hardcodes ``True`` in. 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 + def _tag(value): if isinstance(value, tuple): @@ -111,14 +116,47 @@ session_json_serializer = TaggedJSONSerializer() class SecureCookieSession(CallbackDict, SessionMixin): - """Base class for sessions based on signed cookies.""" + """Base class for sessions based on signed cookies. + + This session backend will set the :attr:`modified` and + :attr:`accessed` attributes. It cannot reliably track whether a + session is new (vs. empty), so :attr:`new` remains hard coded to + ``False``. + """ + + #: When data is changed, this is set to ``True``. Only the session + #: dictionary itself is tracked; if the session contains mutable + #: data (for example a nested dict) then this must be set to + #: ``True`` manually when modifying that data. The session cookie + #: 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=None): def on_update(self): self.modified = True + self.accessed = True CallbackDict.__init__(self, initial, on_update) self.modified = False + def __getitem__(self, key): + self.accessed = True + return super().__getitem__(key) + + def get(self, key, default=None): + self.accessed = True + return super().get(key, default) + + def setdefault(self, key, default=None): + 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/tests/test_basic.py b/tests/test_basic.py index 85555300..5ad8c3de 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -192,12 +192,20 @@ def test_session(): @app.route('/set', methods=['POST']) def set(): + assert not flask.session.accessed + assert not flask.session.modified 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 c = app.test_client() -- 2.40.1