Skip to content

Commit c17f379

Browse files
committed
request context tracks session access
1 parent 27be933 commit c17f379

File tree

6 files changed

+74
-53
lines changed

6 files changed

+74
-53
lines changed

CHANGES.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ Version 3.1.3
33

44
Unreleased
55

6+
- The session is marked as accessed for operations that only access the keys
7+
but not the values, such as ``in`` and ``len``. :ghsa:`68rp-wp8r-4726`
8+
69

710
Version 3.1.2
811
-------------

src/flask/app.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1318,8 +1318,8 @@ def process_response(self, response: Response) -> Response:
13181318
for func in reversed(self.after_request_funcs[name]):
13191319
response = self.ensure_sync(func)(response)
13201320

1321-
if not self.session_interface.is_null_session(ctx.session):
1322-
self.session_interface.save_session(self, ctx.session, response)
1321+
if not self.session_interface.is_null_session(ctx._session):
1322+
self.session_interface.save_session(self, ctx._session, response)
13231323

13241324
return response
13251325

src/flask/ctx.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ def __init__(
324324
except HTTPException as e:
325325
self.request.routing_exception = e
326326
self.flashes: list[tuple[str, str]] | None = None
327-
self.session: SessionMixin | None = session
327+
self._session: SessionMixin | None = session
328328
# Functions that should be executed after the request on the response
329329
# object. These will be called before the regular "after_request"
330330
# functions.
@@ -351,7 +351,7 @@ def copy(self) -> RequestContext:
351351
self.app,
352352
environ=self.request.environ,
353353
request=self.request,
354-
session=self.session,
354+
session=self._session,
355355
)
356356

357357
def match_request(self) -> None:
@@ -364,6 +364,16 @@ def match_request(self) -> None:
364364
except HTTPException as e:
365365
self.request.routing_exception = e
366366

367+
@property
368+
def session(self) -> SessionMixin:
369+
"""The session data associated with this request. Not available until
370+
this context has been pushed. Accessing this property, also accessed by
371+
the :data:`~flask.session` proxy, sets :attr:`.SessionMixin.accessed`.
372+
"""
373+
assert self._session is not None, "The session has not yet been opened."
374+
self._session.accessed = True
375+
return self._session
376+
367377
def push(self) -> None:
368378
# Before we push the request context we have to ensure that there
369379
# is an application context.
@@ -381,12 +391,12 @@ def push(self) -> None:
381391
# This allows a custom open_session method to use the request context.
382392
# Only open a new session if this is the first time the request was
383393
# pushed, otherwise stream_with_context loses the session.
384-
if self.session is None:
394+
if self._session is None:
385395
session_interface = self.app.session_interface
386-
self.session = session_interface.open_session(self.app, self.request)
396+
self._session = session_interface.open_session(self.app, self.request)
387397

388-
if self.session is None:
389-
self.session = session_interface.make_null_session(self.app)
398+
if self._session is None:
399+
self._session = session_interface.make_null_session(self.app)
390400

391401
# Match the request URL after loading the session, so that the
392402
# session is available in custom URL converters.

src/flask/sessions.py

Lines changed: 10 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,15 @@ def permanent(self, value: bool) -> None:
4343
#: ``True``.
4444
modified = True
4545

46-
#: Some implementations can detect when session data is read or
47-
#: written and set this when that happens. The mixin default is hard
48-
#: coded to ``True``.
49-
accessed = True
46+
accessed = False
47+
"""Indicates if the session was accessed, even if it was not modified. This
48+
is set when the session object is accessed through the request context,
49+
including the global :data:`.session` proxy. A ``Vary: cookie`` header will
50+
be added if this is ``True``.
51+
52+
.. versionchanged:: 3.1.3
53+
This is tracked by the request context.
54+
"""
5055

5156

5257
class SecureCookieSession(CallbackDict[str, t.Any], SessionMixin):
@@ -65,34 +70,15 @@ class SecureCookieSession(CallbackDict[str, t.Any], SessionMixin):
6570
#: will only be written to the response if this is ``True``.
6671
modified = False
6772

68-
#: When data is read or written, this is set to ``True``. Used by
69-
# :class:`.SecureCookieSessionInterface` to add a ``Vary: Cookie``
70-
#: header, which allows caching proxies to cache different pages for
71-
#: different users.
72-
accessed = False
73-
7473
def __init__(
7574
self,
76-
initial: c.Mapping[str, t.Any] | c.Iterable[tuple[str, t.Any]] | None = None,
75+
initial: c.Mapping[str, t.Any] | None = None,
7776
) -> None:
7877
def on_update(self: te.Self) -> None:
7978
self.modified = True
80-
self.accessed = True
8179

8280
super().__init__(initial, on_update)
8381

84-
def __getitem__(self, key: str) -> t.Any:
85-
self.accessed = True
86-
return super().__getitem__(key)
87-
88-
def get(self, key: str, default: t.Any = None) -> t.Any:
89-
self.accessed = True
90-
return super().get(key, default)
91-
92-
def setdefault(self, key: str, default: t.Any = None) -> t.Any:
93-
self.accessed = True
94-
return super().setdefault(key, default)
95-
9682

9783
class NullSession(SecureCookieSession):
9884
"""Class used to generate nicer error messages if sessions are not

src/flask/templating.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@
2222

2323

2424
def _default_template_ctx_processor() -> dict[str, t.Any]:
25-
"""Default template context processor. Injects `request`,
26-
`session` and `g`.
25+
"""Default template context processor. Replaces the ``request`` and ``g``
26+
proxies with their concrete objects for faster access.
2727
"""
2828
appctx = _cv_app.get(None)
2929
reqctx = _cv_request.get(None)
@@ -32,7 +32,8 @@ def _default_template_ctx_processor() -> dict[str, t.Any]:
3232
rv["g"] = appctx.g
3333
if reqctx is not None:
3434
rv["request"] = reqctx.request
35-
rv["session"] = reqctx.session
35+
# The session proxy cannot be replaced, accessing it gets
36+
# RequestContext.session, which sets session.accessed.
3637
return rv
3738

3839

tests/test_basic.py

Lines changed: 39 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
from werkzeug.routing import RequestRedirect
2121

2222
import flask
23+
from flask.globals import request_ctx
24+
from flask.testing import FlaskClient
2325

2426
require_cpython_gc = pytest.mark.skipif(
2527
python_implementation() != "CPython",
@@ -231,27 +233,46 @@ def index():
231233
assert client.get("/foo/bar").data == b"bar"
232234

233235

234-
def test_session(app, client):
235-
@app.route("/set", methods=["POST"])
236-
def set():
237-
assert not flask.session.accessed
238-
assert not flask.session.modified
236+
def test_session_accessed(app: flask.Flask, client: FlaskClient) -> None:
237+
@app.post("/")
238+
def do_set():
239239
flask.session["value"] = flask.request.form["value"]
240-
assert flask.session.accessed
241-
assert flask.session.modified
242240
return "value set"
243241

244-
@app.route("/get")
245-
def get():
246-
assert not flask.session.accessed
247-
assert not flask.session.modified
248-
v = flask.session.get("value", "None")
249-
assert flask.session.accessed
250-
assert not flask.session.modified
251-
return v
252-
253-
assert client.post("/set", data={"value": "42"}).data == b"value set"
254-
assert client.get("/get").data == b"42"
242+
@app.get("/")
243+
def do_get():
244+
return flask.session.get("value", "None")
245+
246+
@app.get("/nothing")
247+
def do_nothing() -> str:
248+
return ""
249+
250+
with client:
251+
rv = client.get("/nothing")
252+
assert "cookie" not in rv.vary
253+
assert not request_ctx._session.accessed
254+
assert not request_ctx._session.modified
255+
256+
with client:
257+
rv = client.post(data={"value": "42"})
258+
assert rv.text == "value set"
259+
assert "cookie" in rv.vary
260+
assert request_ctx._session.accessed
261+
assert request_ctx._session.modified
262+
263+
with client:
264+
rv = client.get()
265+
assert rv.text == "42"
266+
assert "cookie" in rv.vary
267+
assert request_ctx._session.accessed
268+
assert not request_ctx._session.modified
269+
270+
with client:
271+
rv = client.get("/nothing")
272+
assert rv.text == ""
273+
assert "cookie" not in rv.vary
274+
assert not request_ctx._session.accessed
275+
assert not request_ctx._session.modified
255276

256277

257278
def test_session_path(app, client):

0 commit comments

Comments
 (0)