183 lines
5.8 KiB
Diff
183 lines
5.8 KiB
Diff
From 4195dc236d4286d6d82b3a40dc595a1437afcdc9 Mon Sep 17 00:00:00 2001
|
|
From: "Brian C. Lane" <bcl@redhat.com>
|
|
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
|
|
|