Flask vulnerable to possible disclosure of permanent session cookie due to missing Vary: Cookie header
Research is free — Hunters explains how the bug works, the root-cause code pattern, how the fix addresses it, and how to test whether a target is affected, in chat. Investigate & write exploit is a paid run — the engine reads the advisory and fix commits, then builds and validates a working proof-of-concept exploit with reproduction steps.
Affected versions
Details
When all of the following conditions are met, a response containing data intended for one client may be cached and subsequently sent by a proxy to other clients. If the proxy also caches `Set-Cookie` headers, it may send one client's `session` cookie to other clients. The severity depends on the application's use of the session, and the proxy's behavior regarding cookies. The risk depends on _all_ these conditions being met. 1. The application must be hosted behind a caching proxy that does not strip cookies or ignore responses with cookies. 2. The application sets [`session.permanent = True`](https://flask.palletsprojects.com/en/2.3.x/api/#flask.session.permanent). 2. The application does not access or modify the session at any point during a request. 4. [`SESSION_REFRESH_EACH_REQUEST`](https://flask.palletsprojects.com/en/2.3.x/config/#SESSION_REFRESH_EACH_REQUEST) is enabled (the default). 5. The application does not set a `Cache-Control` header to indicate that a page is private or should not be cached. This happens because vulnerable versions of Flask only set the `Vary: Cookie` header when the session is accessed or modified, not when it is refreshed (re-sent to update the expiration) without being accessed or modified.
The fix
set `Vary: Cookie` header consistently for session
src/flask/sessions.py+6 −4
@@ -329,6 +329,10 @@ def save_session(samesite = self.get_cookie_samesite(app)httponly = self.get_cookie_httponly(app)+# Add a "Vary: Cookie" header if the session was accessed at all.+if session.accessed:+response.vary.add("Cookie")+# If the session is modified to be empty, remove the cookie.# If the session is empty, return without setting the cookie.if not session:@@ -341,13 +345,10 @@ def save_session(samesite=samesite,httponly=httponly,)+response.vary.add("Cookie")return-# Add a "Vary: Cookie" header if the session was accessed at all.-if session.accessed:-response.vary.add("Cookie")-if not self.should_set_cookie(app, session):return@@ -363,3 +364,4 @@ def save_session(secure=secure,samesite=samesite,)+response.vary.add("Cookie")
tests/test_basic.py+23 −0
@@ -501,6 +501,11 @@ def getitem():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()@@ -533,11 +538,29 @@ def expect(path, header_value="Cookie"):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, client):+@app.get("/login")+def login():+flask.session["user_id"] = 1+flask.session.permanent = True+return ""++@app.get("/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, req_ctx):assert not flask.session.modifiedflask.flash("Zap")
set `Vary: Cookie` header consistently for session
CHANGES.rst+1 −0
@@ -4,6 +4,7 @@ Version 2.2.5Unreleased- Update for compatibility with Werkzeug 2.3.3.+- Set ``Vary: Cookie`` header when the session is accessed, modified, or refreshed.Version 2.2.4
src/flask/sessions.py+6 −4
@@ -383,6 +383,10 @@ def save_session(samesite = self.get_cookie_samesite(app)httponly = self.get_cookie_httponly(app)+# Add a "Vary: Cookie" header if the session was accessed at all.+if session.accessed:+response.vary.add("Cookie")+# If the session is modified to be empty, remove the cookie.# If the session is empty, return without setting the cookie.if not session:@@ -395,13 +399,10 @@ def save_session(samesite=samesite,httponly=httponly,)+response.vary.add("Cookie")return-# Add a "Vary: Cookie" header if the session was accessed at all.-if session.accessed:-response.vary.add("Cookie")-if not self.should_set_cookie(app, session):return@@ -417,3 +418,4 @@ def save_session(secure=secure,samesite=samesite,)+response.vary.add("Cookie")
tests/test_basic.py+23 −0
@@ -560,6 +560,11 @@ def getitem():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()@@ -592,11 +597,29 @@ def expect(path, header_value="Cookie"):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, client):+@app.get("/login")+def login():+flask.session["user_id"] = 1+flask.session.permanent = True+return ""++@app.get("/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, req_ctx):assert not flask.session.modifiedflask.flash("Zap")
References
- WEBhttps://github.com/pallets/flask/security/advisories/GHSA-m2qf-hxjv-5gpq
- ADVISORYhttps://nvd.nist.gov/vuln/detail/CVE-2023-30861
- WEBhttps://github.com/pallets/flask/commit/70f906c51ce49c485f1d355703e9cc3386b1cc2b
- WEBhttps://github.com/pallets/flask/commit/afd63b16170b7c047f5758eb910c416511e9c965
- PACKAGEhttps://github.com/pallets/flask
- WEBhttps://github.com/pallets/flask/releases/tag/2.2.5
- WEBhttps://github.com/pallets/flask/releases/tag/2.3.2
- WEBhttps://github.com/pypa/advisory-database/tree/main/vulns/flask/PYSEC-2023-62.yaml
- WEBhttps://lists.debian.org/debian-lts-announce/2023/08/msg00024.html
- WEBhttps://security.netapp.com/advisory/ntap-20230818-0006
- WEBhttps://www.debian.org/security/2023/dsa-5442