Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/flask/sessions.py: 47%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from __future__ import annotations
3import collections.abc as c
4import hashlib
5import typing as t
6from collections.abc import MutableMapping
7from datetime import datetime
8from datetime import timezone
10from itsdangerous import BadSignature
11from itsdangerous import URLSafeTimedSerializer
12from werkzeug.datastructures import CallbackDict
14from .json.tag import TaggedJSONSerializer
16if t.TYPE_CHECKING: # pragma: no cover
17 import typing_extensions as te
19 from .app import Flask
20 from .wrappers import Request
21 from .wrappers import Response
24class SessionMixin(MutableMapping[str, t.Any]):
25 """Expands a basic dictionary with session attributes."""
27 @property
28 def permanent(self) -> bool:
29 """This reflects the ``'_permanent'`` key in the dict."""
30 return self.get("_permanent", False) # type: ignore[no-any-return]
32 @permanent.setter
33 def permanent(self, value: bool) -> None:
34 self["_permanent"] = bool(value)
36 #: Some implementations can detect whether a session is newly
37 #: created, but that is not guaranteed. Use with caution. The mixin
38 # default is hard-coded ``False``.
39 new = False
41 #: Some implementations can detect changes to the session and set
42 #: this when that happens. The mixin default is hard coded to
43 #: ``True``.
44 modified = 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``.
52 .. versionchanged:: 3.1.3
53 This is tracked by the request context.
54 """
57class SecureCookieSession(CallbackDict[str, t.Any], SessionMixin):
58 """Base class for sessions based on signed cookies.
60 This session backend will set the :attr:`modified` and
61 :attr:`accessed` attributes. It cannot reliably track whether a
62 session is new (vs. empty), so :attr:`new` remains hard coded to
63 ``False``.
64 """
66 #: When data is changed, this is set to ``True``. Only the session
67 #: dictionary itself is tracked; if the session contains mutable
68 #: data (for example a nested dict) then this must be set to
69 #: ``True`` manually when modifying that data. The session cookie
70 #: will only be written to the response if this is ``True``.
71 modified = False
73 def __init__(
74 self,
75 initial: c.Mapping[str, t.Any] | None = None,
76 ) -> None:
77 def on_update(self: te.Self) -> None:
78 self.modified = True
80 super().__init__(initial, on_update)
83class NullSession(SecureCookieSession):
84 """Class used to generate nicer error messages if sessions are not
85 available. Will still allow read-only access to the empty session
86 but fail on setting.
87 """
89 def _fail(self, *args: t.Any, **kwargs: t.Any) -> t.NoReturn:
90 raise RuntimeError(
91 "The session is unavailable because no secret "
92 "key was set. Set the secret_key on the "
93 "application to something unique and secret."
94 )
96 __setitem__ = __delitem__ = clear = pop = popitem = update = setdefault = _fail
97 del _fail
100class SessionInterface:
101 """The basic interface you have to implement in order to replace the
102 default session interface which uses werkzeug's securecookie
103 implementation. The only methods you have to implement are
104 :meth:`open_session` and :meth:`save_session`, the others have
105 useful defaults which you don't need to change.
107 The session object returned by the :meth:`open_session` method has to
108 provide a dictionary like interface plus the properties and methods
109 from the :class:`SessionMixin`. We recommend just subclassing a dict
110 and adding that mixin::
112 class Session(dict, SessionMixin):
113 pass
115 If :meth:`open_session` returns ``None`` Flask will call into
116 :meth:`make_null_session` to create a session that acts as replacement
117 if the session support cannot work because some requirement is not
118 fulfilled. The default :class:`NullSession` class that is created
119 will complain that the secret key was not set.
121 To replace the session interface on an application all you have to do
122 is to assign :attr:`flask.Flask.session_interface`::
124 app = Flask(__name__)
125 app.session_interface = MySessionInterface()
127 Multiple requests with the same session may be sent and handled
128 concurrently. When implementing a new session interface, consider
129 whether reads or writes to the backing store must be synchronized.
130 There is no guarantee on the order in which the session for each
131 request is opened or saved, it will occur in the order that requests
132 begin and end processing.
134 .. versionadded:: 0.8
135 """
137 #: :meth:`make_null_session` will look here for the class that should
138 #: be created when a null session is requested. Likewise the
139 #: :meth:`is_null_session` method will perform a typecheck against
140 #: this type.
141 null_session_class = NullSession
143 #: A flag that indicates if the session interface is pickle based.
144 #: This can be used by Flask extensions to make a decision in regards
145 #: to how to deal with the session object.
146 #:
147 #: .. versionadded:: 0.10
148 pickle_based = False
150 def make_null_session(self, app: Flask) -> NullSession:
151 """Creates a null session which acts as a replacement object if the
152 real session support could not be loaded due to a configuration
153 error. This mainly aids the user experience because the job of the
154 null session is to still support lookup without complaining but
155 modifications are answered with a helpful error message of what
156 failed.
158 This creates an instance of :attr:`null_session_class` by default.
159 """
160 return self.null_session_class()
162 def is_null_session(self, obj: object) -> bool:
163 """Checks if a given object is a null session. Null sessions are
164 not asked to be saved.
166 This checks if the object is an instance of :attr:`null_session_class`
167 by default.
168 """
169 return isinstance(obj, self.null_session_class)
171 def get_cookie_name(self, app: Flask) -> str:
172 """The name of the session cookie. Uses``app.config["SESSION_COOKIE_NAME"]``."""
173 return app.config["SESSION_COOKIE_NAME"] # type: ignore[no-any-return]
175 def get_cookie_domain(self, app: Flask) -> str | None:
176 """The value of the ``Domain`` parameter on the session cookie. If not set,
177 browsers will only send the cookie to the exact domain it was set from.
178 Otherwise, they will send it to any subdomain of the given value as well.
180 Uses the :data:`SESSION_COOKIE_DOMAIN` config.
182 .. versionchanged:: 2.3
183 Not set by default, does not fall back to ``SERVER_NAME``.
184 """
185 return app.config["SESSION_COOKIE_DOMAIN"] # type: ignore[no-any-return]
187 def get_cookie_path(self, app: Flask) -> str:
188 """Returns the path for which the cookie should be valid. The
189 default implementation uses the value from the ``SESSION_COOKIE_PATH``
190 config var if it's set, and falls back to ``APPLICATION_ROOT`` or
191 uses ``/`` if it's ``None``.
192 """
193 return app.config["SESSION_COOKIE_PATH"] or app.config["APPLICATION_ROOT"] # type: ignore[no-any-return]
195 def get_cookie_httponly(self, app: Flask) -> bool:
196 """Returns True if the session cookie should be httponly. This
197 currently just returns the value of the ``SESSION_COOKIE_HTTPONLY``
198 config var.
199 """
200 return app.config["SESSION_COOKIE_HTTPONLY"] # type: ignore[no-any-return]
202 def get_cookie_secure(self, app: Flask) -> bool:
203 """Returns True if the cookie should be secure. This currently
204 just returns the value of the ``SESSION_COOKIE_SECURE`` setting.
205 """
206 return app.config["SESSION_COOKIE_SECURE"] # type: ignore[no-any-return]
208 def get_cookie_samesite(self, app: Flask) -> str | None:
209 """Return ``'Strict'`` or ``'Lax'`` if the cookie should use the
210 ``SameSite`` attribute. This currently just returns the value of
211 the :data:`SESSION_COOKIE_SAMESITE` setting.
212 """
213 return app.config["SESSION_COOKIE_SAMESITE"] # type: ignore[no-any-return]
215 def get_cookie_partitioned(self, app: Flask) -> bool:
216 """Returns True if the cookie should be partitioned. By default, uses
217 the value of :data:`SESSION_COOKIE_PARTITIONED`.
219 .. versionadded:: 3.1
220 """
221 return app.config["SESSION_COOKIE_PARTITIONED"] # type: ignore[no-any-return]
223 def get_expiration_time(self, app: Flask, session: SessionMixin) -> datetime | None:
224 """A helper method that returns an expiration date for the session
225 or ``None`` if the session is linked to the browser session. The
226 default implementation returns now + the permanent session
227 lifetime configured on the application.
228 """
229 if session.permanent:
230 return datetime.now(timezone.utc) + app.permanent_session_lifetime
231 return None
233 def should_set_cookie(self, app: Flask, session: SessionMixin) -> bool:
234 """Used by session backends to determine if a ``Set-Cookie`` header
235 should be set for this session cookie for this response. If the session
236 has been modified, the cookie is set. If the session is permanent and
237 the ``SESSION_REFRESH_EACH_REQUEST`` config is true, the cookie is
238 always set.
240 This check is usually skipped if the session was deleted.
242 .. versionadded:: 0.11
243 """
245 return session.modified or (
246 session.permanent and app.config["SESSION_REFRESH_EACH_REQUEST"]
247 )
249 def open_session(self, app: Flask, request: Request) -> SessionMixin | None:
250 """This is called at the beginning of each request, after
251 pushing the request context, before matching the URL.
253 This must return an object which implements a dictionary-like
254 interface as well as the :class:`SessionMixin` interface.
256 This will return ``None`` to indicate that loading failed in
257 some way that is not immediately an error. The request
258 context will fall back to using :meth:`make_null_session`
259 in this case.
260 """
261 raise NotImplementedError()
263 def save_session(
264 self, app: Flask, session: SessionMixin, response: Response
265 ) -> None:
266 """This is called at the end of each request, after generating
267 a response, before removing the request context. It is skipped
268 if :meth:`is_null_session` returns ``True``.
269 """
270 raise NotImplementedError()
273session_json_serializer = TaggedJSONSerializer()
276def _lazy_sha1(string: bytes = b"") -> t.Any:
277 """Don't access ``hashlib.sha1`` until runtime. FIPS builds may not include
278 SHA-1, in which case the import and use as a default would fail before the
279 developer can configure something else.
280 """
281 return hashlib.sha1(string)
284class SecureCookieSessionInterface(SessionInterface):
285 """The default session interface that stores sessions in signed cookies
286 through the :mod:`itsdangerous` module.
287 """
289 #: the salt that should be applied on top of the secret key for the
290 #: signing of cookie based sessions.
291 salt = "cookie-session"
292 #: the hash function to use for the signature. The default is sha1
293 digest_method = staticmethod(_lazy_sha1)
294 #: the name of the itsdangerous supported key derivation. The default
295 #: is hmac.
296 key_derivation = "hmac"
297 #: A python serializer for the payload. The default is a compact
298 #: JSON derived serializer with support for some extra Python types
299 #: such as datetime objects or tuples.
300 serializer = session_json_serializer
301 session_class = SecureCookieSession
303 def get_signing_serializer(self, app: Flask) -> URLSafeTimedSerializer | None:
304 if not app.secret_key:
305 return None
307 keys: list[str | bytes] = []
309 if fallbacks := app.config["SECRET_KEY_FALLBACKS"]:
310 keys.extend(fallbacks)
312 keys.append(app.secret_key) # itsdangerous expects current key at top
313 return URLSafeTimedSerializer(
314 keys, # type: ignore[arg-type]
315 salt=self.salt,
316 serializer=self.serializer,
317 signer_kwargs={
318 "key_derivation": self.key_derivation,
319 "digest_method": self.digest_method,
320 },
321 )
323 def open_session(self, app: Flask, request: Request) -> SecureCookieSession | None:
324 s = self.get_signing_serializer(app)
325 if s is None:
326 return None
327 val = request.cookies.get(self.get_cookie_name(app))
328 if not val:
329 return self.session_class()
330 max_age = int(app.permanent_session_lifetime.total_seconds())
331 try:
332 data = s.loads(val, max_age=max_age)
333 return self.session_class(data)
334 except BadSignature:
335 return self.session_class()
337 def save_session(
338 self, app: Flask, session: SessionMixin, response: Response
339 ) -> None:
340 name = self.get_cookie_name(app)
341 domain = self.get_cookie_domain(app)
342 path = self.get_cookie_path(app)
343 secure = self.get_cookie_secure(app)
344 partitioned = self.get_cookie_partitioned(app)
345 samesite = self.get_cookie_samesite(app)
346 httponly = self.get_cookie_httponly(app)
348 # Add a "Vary: Cookie" header if the session was accessed at all.
349 if session.accessed:
350 response.vary.add("Cookie")
352 # If the session is modified to be empty, remove the cookie.
353 # If the session is empty, return without setting the cookie.
354 if not session:
355 if session.modified:
356 response.delete_cookie(
357 name,
358 domain=domain,
359 path=path,
360 secure=secure,
361 partitioned=partitioned,
362 samesite=samesite,
363 httponly=httponly,
364 )
365 response.vary.add("Cookie")
367 return
369 if not self.should_set_cookie(app, session):
370 return
372 expires = self.get_expiration_time(app, session)
373 val = self.get_signing_serializer(app).dumps(dict(session)) # type: ignore[union-attr]
374 response.set_cookie(
375 name,
376 val,
377 expires=expires,
378 httponly=httponly,
379 domain=domain,
380 path=path,
381 secure=secure,
382 partitioned=partitioned,
383 samesite=samesite,
384 )
385 response.vary.add("Cookie")