request context tracks session access
This commit is contained in:
parent
27be933840
commit
c17f379390
6 changed files with 73 additions and 52 deletions
|
|
@ -3,6 +3,9 @@ Version 3.1.3
|
||||||
|
|
||||||
Unreleased
|
Unreleased
|
||||||
|
|
||||||
|
- The session is marked as accessed for operations that only access the keys
|
||||||
|
but not the values, such as ``in`` and ``len``. :ghsa:`68rp-wp8r-4726`
|
||||||
|
|
||||||
|
|
||||||
Version 3.1.2
|
Version 3.1.2
|
||||||
-------------
|
-------------
|
||||||
|
|
|
||||||
|
|
@ -1318,8 +1318,8 @@ class Flask(App):
|
||||||
for func in reversed(self.after_request_funcs[name]):
|
for func in reversed(self.after_request_funcs[name]):
|
||||||
response = self.ensure_sync(func)(response)
|
response = self.ensure_sync(func)(response)
|
||||||
|
|
||||||
if not self.session_interface.is_null_session(ctx.session):
|
if not self.session_interface.is_null_session(ctx._session):
|
||||||
self.session_interface.save_session(self, ctx.session, response)
|
self.session_interface.save_session(self, ctx._session, response)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -324,7 +324,7 @@ class RequestContext:
|
||||||
except HTTPException as e:
|
except HTTPException as e:
|
||||||
self.request.routing_exception = e
|
self.request.routing_exception = e
|
||||||
self.flashes: list[tuple[str, str]] | None = None
|
self.flashes: list[tuple[str, str]] | None = None
|
||||||
self.session: SessionMixin | None = session
|
self._session: SessionMixin | None = session
|
||||||
# Functions that should be executed after the request on the response
|
# Functions that should be executed after the request on the response
|
||||||
# object. These will be called before the regular "after_request"
|
# object. These will be called before the regular "after_request"
|
||||||
# functions.
|
# functions.
|
||||||
|
|
@ -351,7 +351,7 @@ class RequestContext:
|
||||||
self.app,
|
self.app,
|
||||||
environ=self.request.environ,
|
environ=self.request.environ,
|
||||||
request=self.request,
|
request=self.request,
|
||||||
session=self.session,
|
session=self._session,
|
||||||
)
|
)
|
||||||
|
|
||||||
def match_request(self) -> None:
|
def match_request(self) -> None:
|
||||||
|
|
@ -364,6 +364,16 @@ class RequestContext:
|
||||||
except HTTPException as e:
|
except HTTPException as e:
|
||||||
self.request.routing_exception = e
|
self.request.routing_exception = e
|
||||||
|
|
||||||
|
@property
|
||||||
|
def session(self) -> SessionMixin:
|
||||||
|
"""The session data associated with this request. Not available until
|
||||||
|
this context has been pushed. Accessing this property, also accessed by
|
||||||
|
the :data:`~flask.session` proxy, sets :attr:`.SessionMixin.accessed`.
|
||||||
|
"""
|
||||||
|
assert self._session is not None, "The session has not yet been opened."
|
||||||
|
self._session.accessed = True
|
||||||
|
return self._session
|
||||||
|
|
||||||
def push(self) -> None:
|
def push(self) -> None:
|
||||||
# Before we push the request context we have to ensure that there
|
# Before we push the request context we have to ensure that there
|
||||||
# is an application context.
|
# is an application context.
|
||||||
|
|
@ -381,12 +391,12 @@ class RequestContext:
|
||||||
# This allows a custom open_session method to use the request context.
|
# This allows a custom open_session method to use the request context.
|
||||||
# Only open a new session if this is the first time the request was
|
# Only open a new session if this is the first time the request was
|
||||||
# pushed, otherwise stream_with_context loses the session.
|
# pushed, otherwise stream_with_context loses the session.
|
||||||
if self.session is None:
|
if self._session is None:
|
||||||
session_interface = self.app.session_interface
|
session_interface = self.app.session_interface
|
||||||
self.session = session_interface.open_session(self.app, self.request)
|
self._session = session_interface.open_session(self.app, self.request)
|
||||||
|
|
||||||
if self.session is None:
|
if self._session is None:
|
||||||
self.session = session_interface.make_null_session(self.app)
|
self._session = session_interface.make_null_session(self.app)
|
||||||
|
|
||||||
# Match the request URL after loading the session, so that the
|
# Match the request URL after loading the session, so that the
|
||||||
# session is available in custom URL converters.
|
# session is available in custom URL converters.
|
||||||
|
|
|
||||||
|
|
@ -43,10 +43,15 @@ class SessionMixin(MutableMapping[str, t.Any]):
|
||||||
#: ``True``.
|
#: ``True``.
|
||||||
modified = True
|
modified = True
|
||||||
|
|
||||||
#: Some implementations can detect when session data is read or
|
accessed = False
|
||||||
#: written and set this when that happens. The mixin default is hard
|
"""Indicates if the session was accessed, even if it was not modified. This
|
||||||
#: coded to ``True``.
|
is set when the session object is accessed through the request context,
|
||||||
accessed = True
|
including the global :data:`.session` proxy. A ``Vary: cookie`` header will
|
||||||
|
be added if this is ``True``.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.1.3
|
||||||
|
This is tracked by the request context.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class SecureCookieSession(CallbackDict[str, t.Any], SessionMixin):
|
class SecureCookieSession(CallbackDict[str, t.Any], SessionMixin):
|
||||||
|
|
@ -65,34 +70,15 @@ class SecureCookieSession(CallbackDict[str, t.Any], SessionMixin):
|
||||||
#: will only be written to the response if this is ``True``.
|
#: will only be written to the response if this is ``True``.
|
||||||
modified = False
|
modified = False
|
||||||
|
|
||||||
#: When data is read or written, this is set to ``True``. Used by
|
|
||||||
# :class:`.SecureCookieSessionInterface` to add a ``Vary: Cookie``
|
|
||||||
#: header, which allows caching proxies to cache different pages for
|
|
||||||
#: different users.
|
|
||||||
accessed = False
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
initial: c.Mapping[str, t.Any] | c.Iterable[tuple[str, t.Any]] | None = None,
|
initial: c.Mapping[str, t.Any] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
def on_update(self: te.Self) -> None:
|
def on_update(self: te.Self) -> None:
|
||||||
self.modified = True
|
self.modified = True
|
||||||
self.accessed = True
|
|
||||||
|
|
||||||
super().__init__(initial, on_update)
|
super().__init__(initial, on_update)
|
||||||
|
|
||||||
def __getitem__(self, key: str) -> t.Any:
|
|
||||||
self.accessed = True
|
|
||||||
return super().__getitem__(key)
|
|
||||||
|
|
||||||
def get(self, key: str, default: t.Any = None) -> t.Any:
|
|
||||||
self.accessed = True
|
|
||||||
return super().get(key, default)
|
|
||||||
|
|
||||||
def setdefault(self, key: str, default: t.Any = None) -> t.Any:
|
|
||||||
self.accessed = True
|
|
||||||
return super().setdefault(key, default)
|
|
||||||
|
|
||||||
|
|
||||||
class NullSession(SecureCookieSession):
|
class NullSession(SecureCookieSession):
|
||||||
"""Class used to generate nicer error messages if sessions are not
|
"""Class used to generate nicer error messages if sessions are not
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,8 @@ if t.TYPE_CHECKING: # pragma: no cover
|
||||||
|
|
||||||
|
|
||||||
def _default_template_ctx_processor() -> dict[str, t.Any]:
|
def _default_template_ctx_processor() -> dict[str, t.Any]:
|
||||||
"""Default template context processor. Injects `request`,
|
"""Default template context processor. Replaces the ``request`` and ``g``
|
||||||
`session` and `g`.
|
proxies with their concrete objects for faster access.
|
||||||
"""
|
"""
|
||||||
appctx = _cv_app.get(None)
|
appctx = _cv_app.get(None)
|
||||||
reqctx = _cv_request.get(None)
|
reqctx = _cv_request.get(None)
|
||||||
|
|
@ -32,7 +32,8 @@ def _default_template_ctx_processor() -> dict[str, t.Any]:
|
||||||
rv["g"] = appctx.g
|
rv["g"] = appctx.g
|
||||||
if reqctx is not None:
|
if reqctx is not None:
|
||||||
rv["request"] = reqctx.request
|
rv["request"] = reqctx.request
|
||||||
rv["session"] = reqctx.session
|
# The session proxy cannot be replaced, accessing it gets
|
||||||
|
# RequestContext.session, which sets session.accessed.
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ from werkzeug.routing import BuildError
|
||||||
from werkzeug.routing import RequestRedirect
|
from werkzeug.routing import RequestRedirect
|
||||||
|
|
||||||
import flask
|
import flask
|
||||||
|
from flask.globals import request_ctx
|
||||||
|
from flask.testing import FlaskClient
|
||||||
|
|
||||||
require_cpython_gc = pytest.mark.skipif(
|
require_cpython_gc = pytest.mark.skipif(
|
||||||
python_implementation() != "CPython",
|
python_implementation() != "CPython",
|
||||||
|
|
@ -231,27 +233,46 @@ def test_endpoint_decorator(app, client):
|
||||||
assert client.get("/foo/bar").data == b"bar"
|
assert client.get("/foo/bar").data == b"bar"
|
||||||
|
|
||||||
|
|
||||||
def test_session(app, client):
|
def test_session_accessed(app: flask.Flask, client: FlaskClient) -> None:
|
||||||
@app.route("/set", methods=["POST"])
|
@app.post("/")
|
||||||
def set():
|
def do_set():
|
||||||
assert not flask.session.accessed
|
|
||||||
assert not flask.session.modified
|
|
||||||
flask.session["value"] = flask.request.form["value"]
|
flask.session["value"] = flask.request.form["value"]
|
||||||
assert flask.session.accessed
|
|
||||||
assert flask.session.modified
|
|
||||||
return "value set"
|
return "value set"
|
||||||
|
|
||||||
@app.route("/get")
|
@app.get("/")
|
||||||
def get():
|
def do_get():
|
||||||
assert not flask.session.accessed
|
return flask.session.get("value", "None")
|
||||||
assert not flask.session.modified
|
|
||||||
v = flask.session.get("value", "None")
|
|
||||||
assert flask.session.accessed
|
|
||||||
assert not flask.session.modified
|
|
||||||
return v
|
|
||||||
|
|
||||||
assert client.post("/set", data={"value": "42"}).data == b"value set"
|
@app.get("/nothing")
|
||||||
assert client.get("/get").data == b"42"
|
def do_nothing() -> str:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
with client:
|
||||||
|
rv = client.get("/nothing")
|
||||||
|
assert "cookie" not in rv.vary
|
||||||
|
assert not request_ctx._session.accessed
|
||||||
|
assert not request_ctx._session.modified
|
||||||
|
|
||||||
|
with client:
|
||||||
|
rv = client.post(data={"value": "42"})
|
||||||
|
assert rv.text == "value set"
|
||||||
|
assert "cookie" in rv.vary
|
||||||
|
assert request_ctx._session.accessed
|
||||||
|
assert request_ctx._session.modified
|
||||||
|
|
||||||
|
with client:
|
||||||
|
rv = client.get()
|
||||||
|
assert rv.text == "42"
|
||||||
|
assert "cookie" in rv.vary
|
||||||
|
assert request_ctx._session.accessed
|
||||||
|
assert not request_ctx._session.modified
|
||||||
|
|
||||||
|
with client:
|
||||||
|
rv = client.get("/nothing")
|
||||||
|
assert rv.text == ""
|
||||||
|
assert "cookie" not in rv.vary
|
||||||
|
assert not request_ctx._session.accessed
|
||||||
|
assert not request_ctx._session.modified
|
||||||
|
|
||||||
|
|
||||||
def test_session_path(app, client):
|
def test_session_path(app, client):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue