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