diff --git a/SOURCES/0002-Added-SESSION_REFRESH_EACH_REQUEST-config-option.patch b/SOURCES/0002-Added-SESSION_REFRESH_EACH_REQUEST-config-option.patch new file mode 100644 index 0000000..626a75b --- /dev/null +++ b/SOURCES/0002-Added-SESSION_REFRESH_EACH_REQUEST-config-option.patch @@ -0,0 +1,173 @@ +From ed323b03c6af8dae6d3dd3f1c3543f00085c9bfd Mon Sep 17 00:00:00 2001 +From: Armin Ronacher +Date: Tue, 30 Jul 2013 16:43:54 +0200 +Subject: [PATCH 2/6] Added SESSION_REFRESH_EACH_REQUEST config option. + +This also changes how sessions are being refreshed. With the new +behavior set-cookie is only emitted if the session is modified or if the +session is permanent. Permanent sessions can be set to not refresh +automatically through the SESSION_REFRESH_EACH_REQUEST config key. + +This fixes #798. +--- + docs/config.rst | 12 +++++++++++ + flask/app.py | 1 + + flask/sessions.py | 33 ++++++++++++++++++++++++++++++ + flask/testsuite/basic.py | 43 ++++++++++++++++++++++++++++++++++++++++ + 4 files changed, 89 insertions(+) + +diff --git a/docs/config.rst b/docs/config.rst +index ced2ad82..1bc46afa 100644 +--- a/docs/config.rst ++++ b/docs/config.rst +@@ -88,6 +88,15 @@ The following configuration values are used internally by Flask: + :class:`datetime.timedelta` object. + Starting with Flask 0.8 this can also be + an integer representing seconds. ++``SESSION_REFRESH_EACH_REQUEST`` this flag controls how permanent ++ sessions are refresh. If set to `True` ++ (which is the default) then the cookie ++ is refreshed each request which ++ automatically bumps the lifetime. If ++ set to `False` a `set-cookie` header is ++ only sent if the session is modified. ++ Non permanent sessions are not affected ++ by this. + ``USE_X_SENDFILE`` enable/disable x-sendfile + ``LOGGER_NAME`` the name of the logger + ``SERVER_NAME`` the name and port number of the server. +@@ -210,6 +219,9 @@ The following configuration values are used internally by Flask: + .. versionadded:: 0.10 + ``JSON_AS_ASCII``, ``JSON_SORT_KEYS``, ``JSONIFY_PRETTYPRINT_REGULAR`` + ++.. versionadded:: 1.0 ++ ``SESSION_REFRESH_EACH_REQUEST`` ++ + Configuring from Files + ---------------------- + +diff --git a/flask/app.py b/flask/app.py +index addc40b4..652c1809 100644 +--- a/flask/app.py ++++ b/flask/app.py +@@ -285,6 +285,7 @@ class Flask(_PackageBoundObject): + 'SESSION_COOKIE_PATH': None, + 'SESSION_COOKIE_HTTPONLY': True, + 'SESSION_COOKIE_SECURE': False, ++ 'SESSION_REFRESH_EACH_REQUEST': True, + 'MAX_CONTENT_LENGTH': None, + 'SEND_FILE_MAX_AGE_DEFAULT': 12 * 60 * 60, # 12 hours + 'TRAP_BAD_REQUEST_ERRORS': False, +diff --git a/flask/sessions.py b/flask/sessions.py +index 3246eb83..d6b7e5ae 100644 +--- a/flask/sessions.py ++++ b/flask/sessions.py +@@ -252,6 +252,24 @@ class SessionInterface(object): + if session.permanent: + return datetime.utcnow() + app.permanent_session_lifetime + ++ def should_set_cookie(self, app, session): ++ """Indicates weather a cookie should be set now or not. This is ++ used by session backends to figure out if they should emit a ++ set-cookie header or not. The default behavior is controlled by ++ the ``SESSION_REFRESH_EACH_REQUEST`` config variable. If ++ it's set to `False` then a cookie is only set if the session is ++ modified, if set to `True` it's always set if the session is ++ permanent. ++ ++ This check is usually skipped if sessions get deleted. ++ ++ .. versionadded:: 1.0 ++ """ ++ if session.modified: ++ return True ++ save_each = app.config['SESSION_REFRESH_EACH_REQUEST'] ++ return save_each and session.permanent ++ + def open_session(self, app, request): + """This method has to be implemented and must either return `None` + in case the loading failed because of a configuration error or an +@@ -315,11 +333,26 @@ class SecureCookieSessionInterface(SessionInterface): + def save_session(self, app, session, response): + domain = self.get_cookie_domain(app) + path = self.get_cookie_path(app) ++ ++ # Delete case. If there is no session we bail early. ++ # If the session was modified to be empty we remove the ++ # whole cookie. + if not session: + if session.modified: + response.delete_cookie(app.session_cookie_name, + domain=domain, path=path) + return ++ ++ # Modification case. There are upsides and downsides to ++ # emitting a set-cookie header each request. The behavior ++ # is controlled by the :meth:`should_set_cookie` method ++ # which performs a quick check to figure out if the cookie ++ # should be set or not. This is controlled by the ++ # SESSION_REFRESH_EACH_REQUEST config flag as well as ++ # the permanent flag on the session itself. ++ if not self.should_set_cookie(app, session): ++ return ++ + httponly = self.get_cookie_httponly(app) + secure = self.get_cookie_secure(app) + expires = self.get_expiration_time(app, session) +diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py +index 51fd46f2..0fffd4ee 100644 +--- a/flask/testsuite/basic.py ++++ b/flask/testsuite/basic.py +@@ -345,6 +345,49 @@ class BasicFunctionalityTestCase(FlaskTestCase): + self.assert_equal(type(rv['b']), bytes) + self.assert_equal(rv['t'], (1, 2, 3)) + ++ def test_session_cookie_setting(self): ++ app = flask.Flask(__name__) ++ app.testing = True ++ app.secret_key = 'dev key' ++ is_permanent = True ++ ++ @app.route('/bump') ++ def bump(): ++ rv = flask.session['foo'] = flask.session.get('foo', 0) + 1 ++ flask.session.permanent = is_permanent ++ return str(rv) ++ ++ @app.route('/read') ++ def read(): ++ return str(flask.session.get('foo', 0)) ++ ++ def run_test(expect_header): ++ with app.test_client() as c: ++ self.assert_equal(c.get('/bump').data, '1') ++ self.assert_equal(c.get('/bump').data, '2') ++ self.assert_equal(c.get('/bump').data, '3') ++ ++ rv = c.get('/read') ++ set_cookie = rv.headers.get('set-cookie') ++ self.assert_equal(set_cookie is not None, expect_header) ++ self.assert_equal(rv.data, '3') ++ ++ is_permanent = True ++ app.config['SESSION_REFRESH_EACH_REQUEST'] = True ++ run_test(expect_header=True) ++ ++ is_permanent = True ++ app.config['SESSION_REFRESH_EACH_REQUEST'] = False ++ run_test(expect_header=False) ++ ++ is_permanent = False ++ app.config['SESSION_REFRESH_EACH_REQUEST'] = True ++ run_test(expect_header=False) ++ ++ is_permanent = False ++ app.config['SESSION_REFRESH_EACH_REQUEST'] = False ++ run_test(expect_header=False) ++ + def test_flashes(self): + app = flask.Flask(__name__) + app.secret_key = 'testkey' +-- +2.31.1 + diff --git a/SOURCES/0003-set-session-accessed-for-setdefault.patch b/SOURCES/0003-set-session-accessed-for-setdefault.patch new file mode 100644 index 0000000..21be0d7 --- /dev/null +++ b/SOURCES/0003-set-session-accessed-for-setdefault.patch @@ -0,0 +1,28 @@ +From 45cfe8875388e3d26bf83935d62d097b8e94c5cd Mon Sep 17 00:00:00 2001 +From: Josh Boyer +Date: Mon, 15 May 2023 09:37:47 -0400 +Subject: [PATCH 3/6] set session accessed for setdefault + +Backport 5d9dd0b379a63d5a90265f2469f86fbd81b05853 +--- + flask/sessions.py | 4 ++++ + 1 file changed, 4 insertions(+) + +diff --git a/flask/sessions.py b/flask/sessions.py +index d6b7e5ae..29857d23 100644 +--- a/flask/sessions.py ++++ b/flask/sessions.py +@@ -116,6 +116,10 @@ class SecureCookieSession(CallbackDict, SessionMixin): + self.modified = False + + ++ def setdefault(self, key, default=None): ++ self.accessed = True ++ return super(SecureCookieSession, self).setdefault(key, default) ++ + class NullSession(SecureCookieSession): + """Class used to generate nicer error messages if sessions are not + available. Will still allow read-only access to the empty session +-- +2.31.1 + diff --git a/SOURCES/0004-Append-a-Vary-Cookie-header-to-the-response-when-the.patch b/SOURCES/0004-Append-a-Vary-Cookie-header-to-the-response-when-the.patch new file mode 100644 index 0000000..9894098 --- /dev/null +++ b/SOURCES/0004-Append-a-Vary-Cookie-header-to-the-response-when-the.patch @@ -0,0 +1,99 @@ +From 915d149c9c6225c60185c3939c195fb18cd6e71a Mon Sep 17 00:00:00 2001 +From: Mihir Singh +Date: Wed, 16 Apr 2014 17:38:41 -0400 +Subject: [PATCH 4/6] Append a 'Vary: Cookie' header to the response when the + session has been accessed + +--- + flask/sessions.py | 58 ++++++++++++++++++++++++++++++++--------------- + 1 file changed, 40 insertions(+), 18 deletions(-) + +diff --git a/flask/sessions.py b/flask/sessions.py +index 29857d23..359b3453 100644 +--- a/flask/sessions.py ++++ b/flask/sessions.py +@@ -51,6 +51,13 @@ class SessionMixin(object): + #: The default mixin implementation just hardcodes `True` in. + modified = True + ++ #: the accessed variable indicates whether or not the session object has ++ #: been accessed in that request. This allows flask to append a `Vary: ++ #: Cookie` header to the response if the session is being accessed. This ++ #: allows caching proxy servers, like Varnish, to use both the URL and the ++ #: session cookie as keys when caching pages, preventing multiple users ++ #: from being served the same cache. ++ accessed = True + + class TaggedJSONSerializer(object): + """A customized JSON serializer that supports a few extra types that +@@ -112,9 +119,18 @@ class SecureCookieSession(CallbackDict, SessionMixin): + def __init__(self, initial=None): + def on_update(self): + self.modified = True ++ self.accessed = True + CallbackDict.__init__(self, initial, on_update) + self.modified = False ++ self.accessed = False + ++ def __getitem__(self, key): ++ self.accessed = True ++ return super(SecureCookieSession, self).__getitem__(key) ++ ++ def get(self, key, default=None): ++ self.accessed = True ++ return super(SecureCookieSession, self).get(key, default) + + def setdefault(self, key, default=None): + self.accessed = True +@@ -338,24 +354,30 @@ class SecureCookieSessionInterface(SessionInterface): + domain = self.get_cookie_domain(app) + path = self.get_cookie_path(app) + +- # Delete case. If there is no session we bail early. +- # If the session was modified to be empty we remove the +- # whole cookie. +- if not session: +- if session.modified: +- response.delete_cookie(app.session_cookie_name, +- domain=domain, path=path) +- return +- +- # Modification case. There are upsides and downsides to +- # emitting a set-cookie header each request. The behavior +- # is controlled by the :meth:`should_set_cookie` method +- # which performs a quick check to figure out if the cookie +- # should be set or not. This is controlled by the +- # SESSION_REFRESH_EACH_REQUEST config flag as well as +- # the permanent flag on the session itself. +- if not self.should_set_cookie(app, session): +- return ++ if session.accessed: ++ ++ response.headers.add('Vary', 'Cookie') ++ ++ else: ++ ++ # Delete case. If there is no session we bail early. ++ # If the session was modified to be empty we remove the ++ # whole cookie. ++ if not session: ++ if session.modified: ++ response.delete_cookie(app.session_cookie_name, ++ domain=domain, path=path) ++ return ++ ++ # Modification case. There are upsides and downsides to ++ # emitting a set-cookie header each request. The behavior ++ # is controlled by the :meth:`should_set_cookie` method ++ # which performs a quick check to figure out if the cookie ++ # should be set or not. This is controlled by the ++ # SESSION_REFRESH_EACH_REQUEST config flag as well as ++ # the permanent flag on the session itself. ++ if not self.should_set_cookie(app, session): ++ return + + httponly = self.get_cookie_httponly(app) + secure = self.get_cookie_secure(app) +-- +2.31.1 + diff --git a/SOURCES/0005-set-Vary-Cookie-header-consistently-for-session.patch b/SOURCES/0005-set-Vary-Cookie-header-consistently-for-session.patch new file mode 100644 index 0000000..788e342 --- /dev/null +++ b/SOURCES/0005-set-Vary-Cookie-header-consistently-for-session.patch @@ -0,0 +1,79 @@ +From df7b86ebe56d9879a411d4ac8d5e93709d200d64 Mon Sep 17 00:00:00 2001 +From: Josh Boyer +Date: Mon, 15 May 2023 07:00:33 -0400 +Subject: [PATCH 5/6] set Vary: Cookie header consistently for session + +Backport 8705dd3 +--- + flask/sessions.py | 48 +++++++++++++++++++++++------------------------ + 1 file changed, 24 insertions(+), 24 deletions(-) + +diff --git a/flask/sessions.py b/flask/sessions.py +index 359b3453..ed069b23 100644 +--- a/flask/sessions.py ++++ b/flask/sessions.py +@@ -354,30 +354,30 @@ 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.headers.add('Vary', 'Cookie') +- +- else: +- +- # Delete case. If there is no session we bail early. +- # If the session was modified to be empty we remove the +- # whole cookie. +- if not session: +- if session.modified: +- response.delete_cookie(app.session_cookie_name, +- domain=domain, path=path) +- return +- +- # Modification case. There are upsides and downsides to +- # emitting a set-cookie header each request. The behavior +- # is controlled by the :meth:`should_set_cookie` method +- # which performs a quick check to figure out if the cookie +- # should be set or not. This is controlled by the +- # SESSION_REFRESH_EACH_REQUEST config flag as well as +- # the permanent flag on the session itself. +- if not self.should_set_cookie(app, session): +- return ++ 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. ++ if not session: ++ 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 ++ # emitting a set-cookie header each request. The behavior ++ # is controlled by the :meth:`should_set_cookie` method ++ # which performs a quick check to figure out if the cookie ++ # should be set or not. This is controlled by the ++ # SESSION_REFRESH_EACH_REQUEST config flag as well as ++ # the permanent flag on the session itself. ++ if not self.should_set_cookie(app, session): ++ return + + httponly = self.get_cookie_httponly(app) + secure = self.get_cookie_secure(app) +@@ -386,6 +386,6 @@ 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') + + from flask.debughelpers import UnexpectedUnicodeError +-- +2.31.1 + diff --git a/SOURCES/0006-Add-Vary-cookie-tests.patch b/SOURCES/0006-Add-Vary-cookie-tests.patch new file mode 100644 index 0000000..e325d0e --- /dev/null +++ b/SOURCES/0006-Add-Vary-cookie-tests.patch @@ -0,0 +1,128 @@ +From d119ab948352958e5b7de1b9b27a7bffd95cc7dd Mon Sep 17 00:00:00 2001 +From: Josh Boyer +Date: Mon, 15 May 2023 07:26:54 -0400 +Subject: [PATCH 6/6] Add Vary cookie tests + +--- + flask/testsuite/basic.py | 97 +++++++++++++++++++++++++++++++++++++++- + 1 file changed, 95 insertions(+), 2 deletions(-) + +diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py +index 0fffd4ee..eb5d65a8 100644 +--- a/flask/testsuite/basic.py ++++ b/flask/testsuite/basic.py +@@ -18,9 +18,10 @@ from datetime import datetime + from threading import Thread + from flask.testsuite import FlaskTestCase, emits_module_deprecation_warning + from flask._compat import text_type +-from werkzeug.exceptions import BadRequest, NotFound ++import werkzeug.serving ++from werkzeug.exceptions import BadRequest, NotFound, Forbidden + from werkzeug.http import parse_date +-from werkzeug.routing import BuildError ++from werkzeug.routing import BuildError, RequestRedirect + + + class BasicFunctionalityTestCase(FlaskTestCase): +@@ -388,6 +389,98 @@ class BasicFunctionalityTestCase(FlaskTestCase): + app.config['SESSION_REFRESH_EACH_REQUEST'] = False + run_test(expect_header=False) + ++ def test_session_vary_cookie(self): ++ app = flask.Flask(__name__) ++ app.testing = True ++ app.secret_key = 'dev key' ++ is_permanent = True ++ ++ @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 "" ++ ++ c = app.test_client() ++ ++ def expect(path, header_value="Cookie"): ++ rv = c.get(path) ++ ++ if header_value: ++ #print "%s" % rv.headers.get_all("Vary") ++ # 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(self): ++ app = flask.Flask(__name__) ++ app.testing = True ++ app.secret_key = 'key dev' ++ is_permanent = True ++ ++ @app.route("/login") ++ def login(): ++ flask.session["user_id"] = 1 ++ flask.session.permanent = True ++ return "" ++ ++ @app.route("/ignored") ++ def ignored(): ++ return "" ++ ++ c = app.test_client() ++ ++ rv = c.get("/login") ++ assert rv.headers["Vary"] == "Cookie" ++ rv = c.get("/ignored") ++ assert rv.headers["Vary"] == "Cookie" ++ ++ + def test_flashes(self): + app = flask.Flask(__name__) + app.secret_key = 'testkey' +-- +2.31.1 + diff --git a/SPECS/python-flask.spec b/SPECS/python-flask.spec index 011f791..566ae8e 100644 --- a/SPECS/python-flask.spec +++ b/SPECS/python-flask.spec @@ -4,12 +4,14 @@ %{!?python_sitelib: %global python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib()")} %endif +%global __python /usr/bin/python2 + %global srcname Flask %global srcversion 0.10.1 Name: python-flask Version: 0.10.1 -Release: 5%{?dist} +Release: 7%{?dist} Epoch: 1 Summary: A micro-framework for Python based on Werkzeug, Jinja 2 and good intentions @@ -18,6 +20,11 @@ License: BSD URL: http://flask.pocoo.org/ Source0: http://pypi.python.org/packages/source/F/Flask/%{srcname}-%{srcversion}.tar.gz Patch0: CVE-2018-1000656.patch +Patch1: 0002-Added-SESSION_REFRESH_EACH_REQUEST-config-option.patch +Patch2: 0003-set-session-accessed-for-setdefault.patch +Patch3: 0004-Append-a-Vary-Cookie-header-to-the-response-when-the.patch +Patch4: 0005-set-Vary-Cookie-header-consistently-for-session.patch +Patch5: 0006-Add-Vary-cookie-tests.patch BuildArch: noarch BuildRequires: python2-devel python-setuptools python-werkzeug python-sphinx @@ -91,6 +98,11 @@ Documentation and examples for python3-flask. %prep %setup -q -n %{srcname}-%{srcversion} %patch0 -p1 +%patch1 -p1 +%patch2 -p1 +%patch3 -p1 +%patch4 -p1 +%patch5 -p1 %{__sed} -i "/platforms/ a\ requires=['Jinja2 (>=2.4)']," setup.py %if 0%{?with_python3} @@ -181,8 +193,11 @@ popd %changelog +* Mon May 15 2023 Josh Boyer - 1:0.10.1-7 +- Fix CVE-2023-30861 (RHBZ #2196677 2196643) + * Tue Mar 03 2020 Josh Boyer - 1:0.10.1-5 -- Fix CVE-2018-1000656 Denial of Service via crafted JSON (RHBZ #1810065) +- Fix CVE-2018-1000656 Denial of Service via crafted JSON (RHBZ #1633596) * Tue Apr 29 2014 Lokesh Mandvekar - 1:0.10.1-4 - Rebuilt for RHEL 7