VYPR
High severityNVD Advisory· Published May 2, 2023· Updated Feb 13, 2025

Flask vulnerable to possible disclosure of permanent session cookie due to missing Vary: Cookie header

CVE-2023-30861

Description

Flask is a lightweight WSGI web application framework. When all of the following conditions are met, a response containing data intended for one client may be cached and subsequently sent by the 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
  3. The application does not access or modify the session at any point during a request.
  4. SESSION_REFRESH_EACH_REQUEST 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. This issue has been fixed in versions 2.3.2 and 2.2.5.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Under specific caching-proxy conditions, Flask incorrectly omits `Vary: Cookie` headers, enabling session cookie leakage between users.

Vulnerability

Overview

CVE-2023-30861 is a cache-poisoning vulnerability in Flask, the popular Python WSGI web framework. The root cause lies in the save_session() method, which only added a Vary: Cookie response header when the session was explicitly accessed or modified, but not when the session was simply refreshed (i.e., re-sent to update its expiration timestamp without being read or written) [1][2]. This breaks the HTTP caching contract, because the Vary header is the standard way to tell proxies that a response differs based on the request's Cookie header.

Exploitation

Conditions

Exploitation requires a specific configuration: the application must be deployed behind a caching proxy that does not strip or ignore Set-Cookie headers, the session must be configured as permanent (session.permanent = True), and the SESSION_REFRESH_EACH_REQUEST setting must remain enabled (the default) [1]. Additionally, the application must not touch the session during a request and must not set an explicit Cache-Control: private or no-cache header. Under these circumstances, the proxy may cache a response that carries a Set-Cookie header without also receiving a Vary: Cookie directive, causing the cached cookie to be served to other clients [1][3].

Impact

A successfully exploited cache-poisoning attack can result in one user's session cookie being delivered to another user by the intermediary proxy. The severity is application-dependent: if the session contains sensitive data or grants access to privileged functions, this could lead to session hijacking or unauthorized access to another user's account [1][4].

Mitigation

The Flask project fixed the issue in versions 2.3.2 and 2.2.5 by modifying the save_session() method to unconditionally add the Vary: Cookie header whenever a session cookie is set — regardless of whether the session was accessed, modified, or merely refreshed [2][3]. Users should upgrade to one of the patched versions. For those unable to upgrade, reviewing proxy configuration to strip or ignore Set-Cookie headers, or manually setting Cache-Control: private on relevant responses, can serve as a workaround [1].

AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
FlaskPyPI
>= 2.3.0, < 2.3.22.3.2
FlaskPyPI
< 2.2.52.2.5

Affected products

23

Patches

2
afd63b16170b

Merge pull request #5109 from pallets/backport-vary-cookie

https://github.com/pallets/flaskDavid LordMay 2, 2023via ghsa
3 files changed · +30 4
  • CHANGES.rst+1 0 modified
    @@ -4,6 +4,7 @@ Version 2.2.5
     Unreleased
     
     -   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 modified
    @@ -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 modified
    @@ -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.modified
         flask.flash("Zap")
    
70f906c51ce4

Merge pull request from GHSA-m2qf-hxjv-5gpq

https://github.com/pallets/flaskDavid LordMay 1, 2023via ghsa
2 files changed · +29 4
  • src/flask/sessions.py+6 4 modified
    @@ -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 modified
    @@ -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.modified
         flask.flash("Zap")
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

12

News mentions

0

No linked articles in our index yet.