Flask is the backbone of thousands of web apps due to its lightweight and flexible nature. But in 2023, a subtle bug (CVE-2023-30861) highlighted a serious risk when combining Flask’s session management with misconfigured caching proxies. In this deep dive, I’ll show you exactly how session leaks can happen, provide exploit code, and walk you through what you need to do to stay safe.
What’s CVE-2023-30861 About?
In certain setups, Flask apps running behind caching proxies can “cross-pollinate” session cookies, giving one visitor another user’s session cookie. If your app sets sensitive session data (like authentication info), users can end up logged in as each other. The leak is silent, happens only under the right (but not rare) set of conditions—and may be devastating.
You don’t set a privacy-related Cache-Control header in those routes
Under these conditions, every request to a buggy Flask endpoint causes Flask to refresh the session, sending a new Set-Cookie. But if the session is never accessed/modified (for example on a static or status endpoint), Flask forgets to tell the proxy that responses vary by cookie—so the proxy might cache and replay one user's response (including their Set-Cookie) to someone else.
The root cause: An affected Flask version only sets Vary: Cookie if the session is accessed, not when just refreshed.
Let’s see this in action. Here’s a small demo Flask app
# vulnerable_flask_app.py
from flask import Flask, session, make_response
app = Flask(__name__)
app.secret_key = 'supersecret'
app.config['SESSION_PERMANENT'] = True
# Make sure SESSION_REFRESH_EACH_REQUEST is True (default)
@app.route('/public')
def public():
# DO NOT ACCESS OR MODIFY session
resp = make_response("Welcome to the public page!")
# No Cache-Control headers set
return resp
Assume this endpoint /public is behind a caching proxy.
Here’s a mini-proxy demonstrating the caching bug
# simple_proxy.py
import requests
from http.server import BaseHTTPRequestHandler, HTTPServer
cache = {}
class Proxy(BaseHTTPRequestHandler):
def do_GET(self):
cookie = self.headers.get('Cookie', '')
key = ('/public', cookie)
if key in cache:
# Serve cached response
self.send_response(200)
self.end_headers()
self.wfile.write(cache[key])
else:
r = requests.get('http://127...1:500/public';, headers={'Cookie': cookie})
cache[key] = r.content
self.send_response(200)
self.end_headers()
self.wfile.write(r.content)
if __name__ == '__main__':
HTTPServer(('...', 8888), Proxy).serve_forever()
Now:
1. Attacker visits /public—gets a fresh session, with a Set-Cookie: session=... set in their browser.
Proxy caches this response.
3. Victim visits /public—proxy serves the cached response including attacker’s Set-Cookie header!
On a real proxy, unless you set Vary: Cookie or Cache-Control: private, this is exactly what happens.
`bash
curl -i http://localhost:8888/public
`bash
curl -i --cookie "" http://localhost:8888/public
Why is This Dangerous?
- If your app stores logged-in status, user roles, tokens, or other secrets, these can be swapped between users.
Suppose you have a login system using Flask’s session
from flask import Flask, session
app = Flask(__name__)
app.secret_key = 'supersecret'
@app.route('/set_session')
def set_session():
session['user'] = 'alice'
session.permanent = True
return "Session set!"
@app.route('/sensitive_route')
def sensitive_route():
return f"Session: {session.get('user', 'none')}"
If /sensitive_route doesn't access or modify the session (or in more real-world cases, if /public doesn't), responses could be cached and users could mount a session fixation attack.
How To Fix
Upgrade to Flask 2.3.2 or 2.2.5 or above—these set Vary: Cookie when the session is refreshed. See Flask release notes and GHSA-v3p7-6rw3-3j39 advisory.
return response
<br>- Configure your proxy to not cache responses containing Set-Cookie`.
- Never rely on the default cache behavior; be explicit.
---
## References
- Flask 2.3.2 release notes
- GitHub security advisory GHSA-v3p7-6rw3-3j39
- CVE-2023-30861 on NVD
---
## Final Thoughts
This bug is a masterclass in how small missteps—often outside your own code—can result in big vulnerabilities. If you use Flask behind any kind of caching proxy, just bump your Flask version (at least 2.3.2/2.2.5+) ASAP, and always set strict cache-control for anything session-dependent.
Stay patched and safe!
Timeline
Published on: 05/02/2023 18:15:00 UTC
Last modified on: 05/10/2023 03:55:00 UTC