Information Disclosure / Improper isolation in request context (copy_current_request_context)
Description
The commit fixes a security issue in Flask's copy_current_request_context decorator. Previously, the decorator captured a single copied request context (ctx) at decoration time and reused that same context for all invocations. This meant concurrent calls to a function wrapped with copy_current_request_context could operate on a shared copied request context, risking cross-call leakage of request/session data between concurrent executions. The patch changes the logic to capture the original (current) app/request context at invocation and creates a fresh copy for each call (with original.copy()). This ensures each worker/ invocation gets an isolated request context, reducing the risk of information leakage across concurrent requests.
What changed (high level):
- Store the active context at decoration time as original rather than a copied ctx.
- In the wrapper, push a fresh copy of the original context for each call (with original.copy()) instead of reusing a single copied ctx.
- Adjusts the guard to require an active request context via original (original is None check).
- Tests were updated to exercise per-call isolation (thread-based test) and remove greenlet-based tests that relied on shared context across calls.
Vulnerability type: Information Disclosure / Improper isolation in request context within copy_current_request_context (concurrent invocations may leak between calls).
Affected scope: Pre-fix releases that used the older single-copy behavior (prior to this commit). Likely affects 3.1.x releases before this change (the commit targets 3.1.3 as tracked version).
Proof of Concept
from flask import Flask, copy_current_request_context, request, session
from concurrent.futures import ThreadPoolExecutor
import threading
# Demonstration PoC: show per-call context isolation by using a shared variable across concurrent invocations.
# In the vulnerable (pre-fix) version, multiple concurrent invocations of a function wrapped with copy_current_request_context
# could end up sharing the same copied request context. The fix ensures a fresh copy per invocation.
app = Flask(__name__)
app.secret_key = 'secret'
# A global map to observe cross-call leakage in a PoC scenario
leak_map = {}
@app.route('/')
def index():
# Define a function that relies on the request context and session
@copy_current_request_context
def work(i):
# Access request-specific data
uid = request.args.get('uid')
# Mutate something in session to observe isolation
session['caller'] = i
# Record observed values to a per-call key
leak_map[i] = {
'uid': uid,
'caller': session.get('caller')
}
return uid
# Launch two concurrent invocations with different uid values
executor = ThreadPoolExecutor(max_workers=2)
futures = [executor.submit(work, i) for i in (1, 2)]
for f in futures:
f.result()
# Return the observed leakage map for inspection
return str(leak_map)
# To run locally for testing:
# if __name__ == '__main__':
# app.run(debug=True)
# Expected: In the vulnerable pre-fix scenario, both threads could observe/overwrite values in a shared context.
# The fix makes each thread/work invocation copy its own request context, isolating request data per call.
Commit Details
Author: David Lord
Date: 2026-05-02 03:17 UTC
Message:
separate copy per call
Triage Assessment
Vulnerability Type: Information Disclosure / Improper isolation in request context
Confidence: MEDIUM
Reasoning:
The change ensures that each invocation of a function wrapped with copy_current_request_context uses its own copied request context, rather than sharing a single context across calls. This improves isolation between concurrent executions and prevents potential cross-call leakage of request/session data, which could lead to information disclosure or improper access across requests.
Verification Assessment
Vulnerability Type: Information Disclosure / Improper isolation in request context (copy_current_request_context)
Confidence: MEDIUM
Affected Versions: < 3.1.3
Code Diff
diff --git a/pyproject.toml b/pyproject.toml
index 0cb10a5829..1bc3e0e10e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -57,7 +57,6 @@ pre-commit = [
]
tests = [
"asgiref",
- "greenlet",
"pytest",
"python-dotenv",
]
diff --git a/src/flask/ctx.py b/src/flask/ctx.py
index dbd62e7505..d4d0de6519 100644
--- a/src/flask/ctx.py
+++ b/src/flask/ctx.py
@@ -189,18 +189,18 @@ def do_some_work():
.. versionadded:: 0.10
"""
- ctx = _cv_app.get(None)
+ # Store the context that was active when the decorator was applied.
+ original = _cv_app.get(None)
- if ctx is None:
+ if original is None:
raise RuntimeError(
"'copy_current_request_context' can only be used when a"
" request context is active, such as in a view function."
)
- ctx = ctx.copy()
-
def wrapper(*args: t.Any, **kwargs: t.Any) -> t.Any:
- with ctx:
+ # Copy the context before pushing, so each worker acts independently.
+ with original.copy() as ctx:
return ctx.app.ensure_sync(f)(*args, **kwargs)
return update_wrapper(wrapper, f) # type: ignore[return-value]
diff --git a/tests/test_reqctx.py b/tests/test_reqctx.py
index 8df053b8ac..06bed24530 100644
--- a/tests/test_reqctx.py
+++ b/tests/test_reqctx.py
@@ -1,16 +1,15 @@
+from __future__ import annotations
+
+import collections.abc as cabc
import warnings
+from concurrent import futures
import pytest
import flask
-from flask.globals import app_ctx
from flask.sessions import SecureCookieSessionInterface
from flask.sessions import SessionInterface
-
-try:
- from greenlet import greenlet
-except ImportError:
- greenlet = None
+from flask.testing import FlaskClient
def test_teardown_on_pop(app):
@@ -145,61 +144,34 @@ def index():
index()
-@pytest.mark.skipif(greenlet is None, reason="greenlet not installed")
-class TestGreenletContextCopying:
- def test_greenlet_context_copying(self, app, client):
- greenlets = []
-
- @app.route("/")
- def index():
- flask.session["fizz"] = "buzz"
- ctx = app_ctx.copy()
-
- def g():
- assert not flask.request
- assert not flask.current_app
- with ctx:
- assert flask.request
- assert flask.current_app == app
- assert flask.request.path == "/"
- assert flask.request.args["foo"] == "bar"
- assert flask.session.get("fizz") == "buzz"
- assert not flask.request
- return 42
-
- greenlets.append(greenlet(g))
- return "Hello World!"
-
- rv = client.get("/?foo=bar")
- assert rv.data == b"Hello World!"
-
- result = greenlets[0].run()
- assert result == 42
-
- def test_greenlet_context_copying_api(self, app, client):
- greenlets = []
-
- @app.route("/")
- def index():
- flask.session["fizz"] = "buzz"
-
- @flask.copy_current_request_context
- def g():
- assert flask.request
- assert flask.current_app == app
- assert flask.request.path == "/"
- assert flask.request.args["foo"] == "bar"
- assert flask.session.get("fizz") == "buzz"
- return 42
-
- greenlets.append(greenlet(g))
- return "Hello World!"
-
- rv = client.get("/?foo=bar")
- assert rv.data == b"Hello World!"
-
- result = greenlets[0].run()
- assert result == 42
+def test_copy_context_thread(
+ request: pytest.FixtureRequest, app: flask.Flask, client: FlaskClient
+) -> None:
+ executor = futures.ThreadPoolExecutor(max_workers=2)
+ request.addfinalizer(lambda: executor.shutdown(cancel_futures=True))
+ result: cabc.Iterator[int] | None = None
+
+ @app.route("/")
+ def index():
+ flask.session["fizz"] = "buzz"
+
+ @flask.copy_current_request_context
+ def work(n: int) -> int:
+ assert flask.current_app == app
+ assert flask.request.path == "/"
+ assert flask.request.args["foo"] == "bar"
+ assert flask.session["fizz"] == "buzz"
+ return n
+
+ nonlocal result
+ result = executor.map(work, range(10))
+ return "Hello World!"
+
+ rv = client.get(query_string={"foo": "bar"})
+ assert rv.text == "Hello World!"
+
+ assert result is not None
+ assert set(result) == set(range(10))
def test_session_error_pops_context():
diff --git a/uv.lock b/uv.lock
index fe9f6cfa87..b4fbacff6b 100644
--- a/uv.lock
+++ b/uv.lock
@@ -441,7 +441,6 @@ pre-commit = [
]
tests = [
{ name = "asgiref" },
- { name = "greenlet" },
{ name = "pytest" },
{ name = "python-dotenv" },
]
@@ -489,7 +488,6 @@ pre-commit = [
]
tests = [
{ name = "asgiref" },
- { name = "greenlet" },
{ name = "pytest" },
{ name = "python-dotenv" },
]
@@ -517,66 +515,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/4b/29/a0e42b0b80d614aa82929f65cce0d51443eea296802b2094cadc5660321b/gha_update-0.2.0-py3-none-any.whl", hash = "sha256:ec5641bf23f71baa1232fc61b3059fb08456e1b78150d1e9c1bab69b37046e49", size = 5323, upload-time = "2025-07-14T03:13:31.932Z" },
]
-[[package]]
-name = "greenlet"
-version = "3.3.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/a3/51/1664f6b78fc6ebbd98019a1fd730e83fa78f2db7058f72b1463d3612b8db/greenlet-3.3.2.tar.gz", hash = "sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2", size = 188267, upload-time = "2026-02-20T20:54:15.531Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/38/3f/9859f655d11901e7b2996c6e3d33e0caa9a1d4572c3bc61ed0faa64b2f4c/greenlet-3.3.2-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9bc885b89709d901859cf95179ec9f6bb67a3d2bb1f0e88456461bd4b7f8fd0d", size = 277747, upload-time = "2026-02-20T20:16:21.325Z" },
- { url = "https://files.pythonhosted.org/packages/fb/07/cb284a8b5c6498dbd7cba35d31380bb123d7dceaa7907f606c8ff5993cbf/greenlet-3.3.2-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b568183cf65b94919be4438dc28416b234b678c608cafac8874dfeeb2a9bbe13", size = 579202, upload-time = "2026-02-20T20:47:28.955Z" },
- { url = "https://files.pythonhosted.org/packages/ed/45/67922992b3a152f726163b19f890a85129a992f39607a2a53155de3448b8/greenlet-3.3.2-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:527fec58dc9f90efd594b9b700662ed3fb2493c2122067ac9c740d98080a620e", size = 590620, upload-time = "2026-02-20T20:55:55.581Z" },
- { url = "https://files.pythonhosted.org/packages/03/5f/6e2a7d80c353587751ef3d44bb947f0565ec008a2e0927821c007e96d3a7/greenlet-3.3.2-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:508c7f01f1791fbc8e011bd508f6794cb95397fdb198a46cb6635eb5b78d85a7", size = 602132, upload-time = "2026-02-20T21:02:43.261Z" },
- { url = "https://files.pythonhosted.org/packages/ad/55/9f1ebb5a825215fadcc0f7d5073f6e79e3007e3282b14b22d6aba7ca6cb8/greenlet-3.3.2-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ad0c8917dd42a819fe77e6bdfcb84e3379c0de956469301d9fd36427a1ca501f", size = 591729, upload-time = "2026-02-20T20:20:58.395Z" },
- { url = "https://files.pythonhosted.org/packages/24/b4/21f5455773d37f94b866eb3cf5caed88d6cea6dd2c6e1f9c34f463cba3ec/greenlet-3.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:97245cc10e5515dbc8c3104b2928f7f02b6813002770cfaffaf9a6e0fc2b94ef", size = 1551946, upload-time = "2026-02-20T20:49:31.102Z" },
- { url = "https://files.pythonhosted.org/packages/00/68/91f061a926abead128fe1a87f0b453ccf07368666bd59ffa46016627a930/greenlet-3.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8c1fdd7d1b309ff0da81d60a9688a8bd044ac4e18b250320a96fc68d31c209ca", size = 1618494, upload-time = "2026-02-20T20:21:06.541Z" },
- { url = "https://files.pythonhosted.org/packages/ac/78/f93e840cbaef8becaf6adafbaf1319682a6c2d8c1c20224267a5c6c8c891/greenlet-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:5d0e35379f93a6d0222de929a25ab47b5eb35b5ef4721c2b9cbcc4036129ff1f", size = 230092, upload-time = "2026-02-20T20:17:09.379Z" },
- { url = "https://files.pythonhosted.org/packages/f3/47/16400cb42d18d7a6bb46f0626852c1718612e35dcb0dffa16bbaffdf5dd2/greenlet-3.3.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:c56692189a7d1c7606cb794be0a8381470d95c57ce5be03fb3d0ef57c7853b86", size = 278890, upload-time = "2026-02-20T20:19:39.263Z" },
- { url = "https://files.pythonhosted.org/packages/a3/90/42762b77a5b6aa96cd8c0e80612663d39211e8ae8a6cd47c7f1249a66262/greenlet-3.3.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ebd458fa8285960f382841da585e02201b53a5ec2bac6b156fc623b5ce4499f", size = 581120, upload-time = "2026-02-20T20:47:30.161Z" },
- { url = "https://files.pythonhosted.org/packages/bf/6f/f3d64f4fa0a9c7b5c5b3c810ff1df614540d5aa7d519261b53fba55d4df9/greenlet-3.3.2-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a443358b33c4ec7b05b79a7c8b466f5d275025e750298be7340f8fc63dff2a55", size = 594363, upload-time = "2026-02-20T20:55:56.965Z" },
- { url = "https://files.pythonhosted.org/packages/9c/8b/1430a04657735a3f23116c2e0d5eb10220928846e4537a938a41b350bed6/greenlet-3.3.2-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4375a58e49522698d3e70cc0b801c19433021b5c37686f7ce9c65b0d5c8677d2", size = 605046, upload-time = "2026-02-20T21:02:45.234Z" },
- { url = "https://files.pythonhosted.org/packages/72/83/3e06a52aca8128bdd4dcd67e932b809e76a96ab8c232a8b025b2850264c5/greenlet-3.3.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e2cd90d413acbf5e77ae41e5d3c9b3ac1d011a756d7284d7f3f2b806bbd6358", size = 594156, upload-time = "2026-02-20T20:20:59.955Z" },
- { url = "https://files.pythonhosted.org/packages/70/79/0de5e62b873e08fe3cef7dbe84e5c4bc0e8ed0c7ff131bccb8405cd107c8/greenlet-3.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:442b6057453c8cb29b4fb36a2ac689382fc71112273726e2423f7f17dc73bf99", size = 1554649, upload-time = "2026-02-20T20:49:32.293Z" },
- { url = "https://files.pythonhosted.org/packages/5a/00/32d30dee8389dc36d42170a9c66217757289e2afb0de59a3565260f38373/greenlet-3.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45abe8eb6339518180d5a7fa47fa01945414d7cca5ecb745346fc6a87d2750be", size = 1619472, upload-time = "2026-02-20T20:21:07.966Z" },
- { url = "https://files.pythonhosted.org/packages/f1/3a/efb2cf697fbccdf75b24e2c18025e7dfa54c4f31fab75c51d0fe79942cef/greenlet-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e692b2dae4cc7077cbb11b47d258533b48c8fde69a33d0d8a82e2fe8d8531d5", size = 230389, upload-time = "2026-02-20T20:17:18.772Z" },
- { url = "https://files.pythonhosted.org/packages/e1/a1/65bbc059a43a7e2143ec4fc1f9e3f673e04f9c7b371a494a101422ac4fd5/greenlet-3.3.2-cp311-cp311-win_arm64.whl", hash = "sha256:02b0a8682aecd4d3c6c18edf52bc8e51eacdd75c8eac52a790a210b06aa295fd", size = 229645, upload-time = "2026-02-20T20:18:18.695Z" },
- { url = "https://files.pythonhosted.org/packages/ea/ab/1608e5a7578e62113506740b88066bf09888322a311cff602105e619bd87/greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd", size = 280358, upload-time = "2026-02-20T20:17:43.971Z" },
- { url = "https://files.pythonhosted.org/packages/a5/23/0eae412a4ade4e6623ff7626e38998cb9b11e9ff1ebacaa021e4e108ec15/greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd", size = 601217, upload-time = "2026-02-20T20:47:31.462Z" },
- { url = "https://files.pythonhosted.org/packages/f8/16/5b1678a9c07098ecb9ab2dd159fafaf12e963293e61ee8d10ecb55273e5e/greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac", size = 611792, upload-time = "2026-02-20T20:55:58.423Z" },
- { url = "https://files.pythonhosted.org/packages/5c/c5/cc09412a29e43406eba18d61c70baa936e299bc27e074e2be3806ed29098/greenlet-3.3.2-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae9e21c84035c490506c17002f5c8ab25f980205c3e61ddb3a2a2a2e6c411fcb", size = 626250, upload-time = "2026-02-20T21:02:46.596Z" },
- { url = "https://files.pythonhosted.org/packages/50/1f/5155f55bd71cabd03765a4aac9ac446be129895271f73872c36ebd4b04b6/greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070", size = 613875, upload-time = "2026-02-20T20:21:01.102Z" },
- { url = "https://files.pythonhosted.org/packages/fc/dd/845f249c3fcd69e32df80cdab059b4be8b766ef5830a3d0aa9d6cad55beb/greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79", size = 1571467, upload-time = "2026-02-20T20:49:33.495Z" },
- { url = "https://files.pythonhosted.org/packages/2a/50/2649fe21fcc2b56659a452868e695634722a6655ba245d9f77f5656010bf/greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395", size = 1640001, upload-time = "2026-02-20T20:21:09.154Z" },
- { url = "https://files.pythonhosted.org/packages/9b/40/cc802e067d02af8b60b6771cea7d57e21ef5e6659912814babb42b864713/greenlet-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:34308836d8370bddadb41f5a7ce96879b72e2fdfb4e87729330c6ab52376409f", size = 231081, upload-time = "2026-02-20T20:17:28.121Z" },
- { url = "https://files.pythonhosted.org/packages/58/2e/fe7f36ff1982d6b10a60d5e0740c759259a7d6d2e1dc41da6d96de32fff6/greenlet-3.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:d3a62fa76a32b462a97198e4c9e99afb9ab375115e74e9a83ce180e7a496f643", size = 230331, upload-time = "2026-02-20T20:17:23.34Z" },
- { url = "https://files.pythonhosted.org/packages/ac/48/f8b875fa7dea7dd9b33245e37f065af59df6a25af2f9561efa8d822fde51/greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4", size = 279120, upload-time = "2026-02-20T20:19:01.9Z" },
- { url = "https://files.pythonhosted.org/packages/49/8d/9771d03e7a8b1ee456511961e1b97a6d77ae1dea4a34a5b98eee706689d3/greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986", size = 603238, upload-time = "2026-02-20T20:47:32.873Z" },
- { url = "https://files.pythonhosted.org/packages/59/0e/4223c2bbb63cd5c97f28ffb2
... [truncated]