diff --git a/0004-Backport-Vary-Cookie-fix-from-upstream.patch b/0004-Backport-Vary-Cookie-fix-from-upstream.patch new file mode 100644 index 0000000000000000000000000000000000000000..c985c726deedab6ada890a469396d21e35675527 --- /dev/null +++ b/0004-Backport-Vary-Cookie-fix-from-upstream.patch @@ -0,0 +1,182 @@ +From 4195dc236d4286d6d82b3a40dc595a1437afcdc9 Mon Sep 17 00:00:00 2001 +From: "Brian C. Lane" +Date: Tue, 9 May 2023 15:13:34 -0700 +Subject: [PATCH 4/5] Backport Vary: Cookie fix from upstream + +This fixes CVE-2023-30861 by backporting the patch and tests from this +upstream commit: +https://github.com/pallets/flask/commit/70f906c51ce49c485f1d355703e9cc3386b1cc2b + +Resolves: rhbz#2196683 +--- + flask/sessions.py | 6 +++ + tests/test_basic.py | 90 ++++++++++++++++++++++++++++++++++++++++++++- + tests/test_ext.py | 2 +- + 3 files changed, 95 insertions(+), 3 deletions(-) + +diff --git a/flask/sessions.py b/flask/sessions.py +index 525ff246..2f60e7eb 100644 +--- a/flask/sessions.py ++++ b/flask/sessions.py +@@ -338,6 +338,10 @@ class SecureCookieSessionInterface(SessionInterface): + domain = self.get_cookie_domain(app) + path = self.get_cookie_path(app) + ++ # Add a "Vary: Cookie" header if the session was accessed at all. ++ if session.accessed: ++ response.vary.add("Cookie") ++ + # Delete case. If there is no session we bail early. + # If the session was modified to be empty we remove the + # whole cookie. +@@ -345,6 +349,7 @@ class SecureCookieSessionInterface(SessionInterface): + if session.modified: + response.delete_cookie(app.session_cookie_name, + domain=domain, path=path) ++ response.vary.add("Cookie") + return + + # Modification case. There are upsides and downsides to +@@ -364,3 +369,4 @@ class SecureCookieSessionInterface(SessionInterface): + response.set_cookie(app.session_cookie_name, val, + expires=expires, httponly=httponly, + domain=domain, path=path, secure=secure) ++ response.vary.add("Cookie") +diff --git a/tests/test_basic.py b/tests/test_basic.py +index c5ec9f5c..85555300 100644 +--- a/tests/test_basic.py ++++ b/tests/test_basic.py +@@ -11,6 +11,7 @@ + + import pytest + ++import os + import re + import uuid + import time +@@ -196,7 +197,8 @@ def test_session(): + + @app.route('/get') + def get(): +- return flask.session['value'] ++ v = flask.session.get("value", "None") ++ return v + + c = app.test_client() + assert c.post('/set', data={'value': '42'}).data == b'value set' +@@ -333,7 +335,7 @@ def test_session_expiration(): + client = app.test_client() + rv = client.get('/') + assert 'set-cookie' in rv.headers +- match = re.search(r'\bexpires=([^;]+)(?i)', rv.headers['set-cookie']) ++ match = re.search(r'(?i)\bexpires=([^;]+)', rv.headers['set-cookie']) + expires = parse_date(match.group()) + expected = datetime.utcnow() + app.permanent_session_lifetime + assert expires.year == expected.year +@@ -445,6 +447,90 @@ def test_session_cookie_setting(): + run_test(expect_header=False) + + ++def test_session_vary_cookie(): ++ app = flask.Flask("flask_test", root_path=os.path.dirname(__file__)) ++ app.config.update( ++ TESTING=True, ++ SECRET_KEY="test key", ++ ) ++ client = app.test_client() ++ ++ @app.route("/set") ++ def set_session(): ++ flask.session["test"] = "test" ++ return "" ++ @app.route("/get") ++ def get(): ++ return flask.session.get("test") ++ @app.route("/getitem") ++ def getitem(): ++ return flask.session["test"] ++ @app.route("/setdefault") ++ def setdefault(): ++ return flask.session.setdefault("test", "default") ++ ++ @app.route("/clear") ++ def clear(): ++ flask.session.clear() ++ return "" ++ ++ @app.route("/vary-cookie-header-set") ++ def vary_cookie_header_set(): ++ response = flask.Response() ++ response.vary.add("Cookie") ++ flask.session["test"] = "test" ++ return response ++ @app.route("/vary-header-set") ++ def vary_header_set(): ++ response = flask.Response() ++ response.vary.update(("Accept-Encoding", "Accept-Language")) ++ flask.session["test"] = "test" ++ return response ++ @app.route("/no-vary-header") ++ def no_vary_header(): ++ return "" ++ def expect(path, header_value="Cookie"): ++ rv = client.get(path) ++ if header_value: ++ # The 'Vary' key should exist in the headers only once. ++ assert len(rv.headers.get_all("Vary")) == 1 ++ assert rv.headers["Vary"] == header_value ++ else: ++ assert "Vary" not in rv.headers ++ expect("/set") ++ expect("/get") ++ expect("/getitem") ++ expect("/setdefault") ++ expect("/clear") ++ expect("/vary-cookie-header-set") ++ expect("/vary-header-set", "Accept-Encoding, Accept-Language, Cookie") ++ expect("/no-vary-header", None) ++ ++ ++def test_session_refresh_vary(): ++ app = flask.Flask("flask_test", root_path=os.path.dirname(__file__)) ++ app.config.update( ++ TESTING=True, ++ SECRET_KEY="test key", ++ ) ++ client = app.test_client() ++ ++ @app.route("/login") ++ def login(): ++ flask.session["user_id"] = 1 ++ flask.session.permanent = True ++ return "" ++ ++ @app.route("/ignored") ++ def ignored(): ++ return "" ++ ++ rv = client.get("/login") ++ assert rv.headers["Vary"] == "Cookie" ++ rv = client.get("/ignored") ++ assert rv.headers["Vary"] == "Cookie" ++ ++ + def test_flashes(): + app = flask.Flask(__name__) + app.secret_key = 'testkey' +diff --git a/tests/test_ext.py b/tests/test_ext.py +index d336e404..c3e23cdf 100644 +--- a/tests/test_ext.py ++++ b/tests/test_ext.py +@@ -180,7 +180,7 @@ def test_no_error_swallowing(flaskext_broken): + with pytest.raises(ImportError) as excinfo: + import flask.ext.broken + +- assert excinfo.type is ImportError ++ assert excinfo.type in (ImportError, ModuleNotFoundError) + if PY2: + message = 'No module named missing_module' + else: +-- +2.40.1 + diff --git a/0005-Backport-support-for-the-accessed-attribute.patch b/0005-Backport-support-for-the-accessed-attribute.patch new file mode 100644 index 0000000000000000000000000000000000000000..acf91bd55fd515a59a57037ff362130264c97aad --- /dev/null +++ b/0005-Backport-support-for-the-accessed-attribute.patch @@ -0,0 +1,107 @@ +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 + diff --git a/python-flask.spec b/python-flask.spec index d149dfaeb04f6a75c7f672d520f663737aa29386..f4949b014f1c5de7b072b7c5bfcf868ed402fe9e 100644 --- a/python-flask.spec +++ b/python-flask.spec @@ -10,7 +10,7 @@ Name: python-%{modname} Version: 0.12.2 -Release: 4%{?dist} +Release: 5%{?dist} Epoch: 1 Summary: A micro-framework for Python based on Werkzeug, Jinja 2 and good intentions @@ -28,6 +28,10 @@ Patch0001: 0001-detect-UTF-encodings-when-loading-json.patch Patch0002: 0002-Fix-ValueError-for-some-invalid-Range-requests.patch Patch0003: 0003-be-smarter-about-adding-.cli-to-reloader-command.patch +# https://github.com/pallets/flask/commit/afd63b16170b7c047f5758eb910c416511e9c965 +Patch0004: 0004-Backport-Vary-Cookie-fix-from-upstream.patch +Patch0005: 0005-Backport-support-for-the-accessed-attribute.patch + BuildArch: noarch %global _description \ @@ -166,6 +170,9 @@ PYTHONPATH=%{buildroot}%{python3_sitelib} py.test-%{python3_version} -v || : %doc docs/_build/html examples %changelog +* Tue Jan 02 2024 Bo Liu - 0.12.2-5 +- Fix CVE-2023-30861 + * Thu Nov 07 2019 Brian C. Lane - 0.12.2-4 - Add upstream changes from 0.12.4 Resolves: rhbz#1585318