Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/flask/sessions.py: 53%

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

125 statements  

1from __future__ import annotations 

2 

3import collections.abc as c 

4import hashlib 

5import typing as t 

6from collections.abc import MutableMapping 

7from datetime import datetime 

8from datetime import timezone 

9 

10from itsdangerous import BadSignature 

11from itsdangerous import URLSafeTimedSerializer 

12from werkzeug.datastructures import CallbackDict 

13 

14from .json.tag import TaggedJSONSerializer 

15 

16if t.TYPE_CHECKING: # pragma: no cover 

17 import typing_extensions as te 

18 

19 from .app import Flask 

20 from .wrappers import Request 

21 from .wrappers import Response 

22 

23 

24class SessionMixin(MutableMapping[str, t.Any]): 

25 """Expands a basic dictionary with session attributes.""" 

26 

27 @property 

28 def permanent(self) -> bool: 

29 """This reflects the ``'_permanent'`` key in the dict.""" 

30 return self.get("_permanent", False) 

31 

32 @permanent.setter 

33 def permanent(self, value: bool) -> None: 

34 self["_permanent"] = bool(value) 

35 

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 

40 

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 

45 

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 

50 

51 

52class SecureCookieSession(CallbackDict[str, t.Any], SessionMixin): 

53 """Base class for sessions based on signed cookies. 

54 

55 This session backend will set the :attr:`modified` and 

56 :attr:`accessed` attributes. It cannot reliably track whether a 

57 session is new (vs. empty), so :attr:`new` remains hard coded to 

58 ``False``. 

59 """ 

60 

61 #: When data is changed, this is set to ``True``. Only the session 

62 #: dictionary itself is tracked; if the session contains mutable 

63 #: data (for example a nested dict) then this must be set to 

64 #: ``True`` manually when modifying that data. The session cookie 

65 #: will only be written to the response if this is ``True``. 

66 modified = False 

67 

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 

74 def __init__( 

75 self, 

76 initial: c.Mapping[str, t.Any] | c.Iterable[tuple[str, t.Any]] | None = None, 

77 ) -> None: 

78 def on_update(self: te.Self) -> None: 

79 self.modified = True 

80 self.accessed = True 

81 

82 super().__init__(initial, on_update) 

83 

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 

96 

97class NullSession(SecureCookieSession): 

98 """Class used to generate nicer error messages if sessions are not 

99 available. Will still allow read-only access to the empty session 

100 but fail on setting. 

101 """ 

102 

103 def _fail(self, *args: t.Any, **kwargs: t.Any) -> t.NoReturn: 

104 raise RuntimeError( 

105 "The session is unavailable because no secret " 

106 "key was set. Set the secret_key on the " 

107 "application to something unique and secret." 

108 ) 

109 

110 __setitem__ = __delitem__ = clear = pop = popitem = update = setdefault = _fail # type: ignore # noqa: B950 

111 del _fail 

112 

113 

114class SessionInterface: 

115 """The basic interface you have to implement in order to replace the 

116 default session interface which uses werkzeug's securecookie 

117 implementation. The only methods you have to implement are 

118 :meth:`open_session` and :meth:`save_session`, the others have 

119 useful defaults which you don't need to change. 

120 

121 The session object returned by the :meth:`open_session` method has to 

122 provide a dictionary like interface plus the properties and methods 

123 from the :class:`SessionMixin`. We recommend just subclassing a dict 

124 and adding that mixin:: 

125 

126 class Session(dict, SessionMixin): 

127 pass 

128 

129 If :meth:`open_session` returns ``None`` Flask will call into 

130 :meth:`make_null_session` to create a session that acts as replacement 

131 if the session support cannot work because some requirement is not 

132 fulfilled. The default :class:`NullSession` class that is created 

133 will complain that the secret key was not set. 

134 

135 To replace the session interface on an application all you have to do 

136 is to assign :attr:`flask.Flask.session_interface`:: 

137 

138 app = Flask(__name__) 

139 app.session_interface = MySessionInterface() 

140 

141 Multiple requests with the same session may be sent and handled 

142 concurrently. When implementing a new session interface, consider 

143 whether reads or writes to the backing store must be synchronized. 

144 There is no guarantee on the order in which the session for each 

145 request is opened or saved, it will occur in the order that requests 

146 begin and end processing. 

147 

148 .. versionadded:: 0.8 

149 """ 

150 

151 #: :meth:`make_null_session` will look here for the class that should 

152 #: be created when a null session is requested. Likewise the 

153 #: :meth:`is_null_session` method will perform a typecheck against 

154 #: this type. 

155 null_session_class = NullSession 

156 

157 #: A flag that indicates if the session interface is pickle based. 

158 #: This can be used by Flask extensions to make a decision in regards 

159 #: to how to deal with the session object. 

160 #: 

161 #: .. versionadded:: 0.10 

162 pickle_based = False 

163 

164 def make_null_session(self, app: Flask) -> NullSession: 

165 """Creates a null session which acts as a replacement object if the 

166 real session support could not be loaded due to a configuration 

167 error. This mainly aids the user experience because the job of the 

168 null session is to still support lookup without complaining but 

169 modifications are answered with a helpful error message of what 

170 failed. 

171 

172 This creates an instance of :attr:`null_session_class` by default. 

173 """ 

174 return self.null_session_class() 

175 

176 def is_null_session(self, obj: object) -> bool: 

177 """Checks if a given object is a null session. Null sessions are 

178 not asked to be saved. 

179 

180 This checks if the object is an instance of :attr:`null_session_class` 

181 by default. 

182 """ 

183 return isinstance(obj, self.null_session_class) 

184 

185 def get_cookie_name(self, app: Flask) -> str: 

186 """The name of the session cookie. Uses``app.config["SESSION_COOKIE_NAME"]``.""" 

187 return app.config["SESSION_COOKIE_NAME"] # type: ignore[no-any-return] 

188 

189 def get_cookie_domain(self, app: Flask) -> str | None: 

190 """The value of the ``Domain`` parameter on the session cookie. If not set, 

191 browsers will only send the cookie to the exact domain it was set from. 

192 Otherwise, they will send it to any subdomain of the given value as well. 

193 

194 Uses the :data:`SESSION_COOKIE_DOMAIN` config. 

195 

196 .. versionchanged:: 2.3 

197 Not set by default, does not fall back to ``SERVER_NAME``. 

198 """ 

199 return app.config["SESSION_COOKIE_DOMAIN"] # type: ignore[no-any-return] 

200 

201 def get_cookie_path(self, app: Flask) -> str: 

202 """Returns the path for which the cookie should be valid. The 

203 default implementation uses the value from the ``SESSION_COOKIE_PATH`` 

204 config var if it's set, and falls back to ``APPLICATION_ROOT`` or 

205 uses ``/`` if it's ``None``. 

206 """ 

207 return app.config["SESSION_COOKIE_PATH"] or app.config["APPLICATION_ROOT"] # type: ignore[no-any-return] 

208 

209 def get_cookie_httponly(self, app: Flask) -> bool: 

210 """Returns True if the session cookie should be httponly. This 

211 currently just returns the value of the ``SESSION_COOKIE_HTTPONLY`` 

212 config var. 

213 """ 

214 return app.config["SESSION_COOKIE_HTTPONLY"] # type: ignore[no-any-return] 

215 

216 def get_cookie_secure(self, app: Flask) -> bool: 

217 """Returns True if the cookie should be secure. This currently 

218 just returns the value of the ``SESSION_COOKIE_SECURE`` setting. 

219 """ 

220 return app.config["SESSION_COOKIE_SECURE"] # type: ignore[no-any-return] 

221 

222 def get_cookie_samesite(self, app: Flask) -> str | None: 

223 """Return ``'Strict'`` or ``'Lax'`` if the cookie should use the 

224 ``SameSite`` attribute. This currently just returns the value of 

225 the :data:`SESSION_COOKIE_SAMESITE` setting. 

226 """ 

227 return app.config["SESSION_COOKIE_SAMESITE"] # type: ignore[no-any-return] 

228 

229 def get_cookie_partitioned(self, app: Flask) -> bool: 

230 """Returns True if the cookie should be partitioned. By default, uses 

231 the value of :data:`SESSION_COOKIE_PARTITIONED`. 

232 

233 .. versionadded:: 3.1 

234 """ 

235 return app.config["SESSION_COOKIE_PARTITIONED"] # type: ignore[no-any-return] 

236 

237 def get_expiration_time(self, app: Flask, session: SessionMixin) -> datetime | None: 

238 """A helper method that returns an expiration date for the session 

239 or ``None`` if the session is linked to the browser session. The 

240 default implementation returns now + the permanent session 

241 lifetime configured on the application. 

242 """ 

243 if session.permanent: 

244 return datetime.now(timezone.utc) + app.permanent_session_lifetime 

245 return None 

246 

247 def should_set_cookie(self, app: Flask, session: SessionMixin) -> bool: 

248 """Used by session backends to determine if a ``Set-Cookie`` header 

249 should be set for this session cookie for this response. If the session 

250 has been modified, the cookie is set. If the session is permanent and 

251 the ``SESSION_REFRESH_EACH_REQUEST`` config is true, the cookie is 

252 always set. 

253 

254 This check is usually skipped if the session was deleted. 

255 

256 .. versionadded:: 0.11 

257 """ 

258 

259 return session.modified or ( 

260 session.permanent and app.config["SESSION_REFRESH_EACH_REQUEST"] 

261 ) 

262 

263 def open_session(self, app: Flask, request: Request) -> SessionMixin | None: 

264 """This is called at the beginning of each request, after 

265 pushing the request context, before matching the URL. 

266 

267 This must return an object which implements a dictionary-like 

268 interface as well as the :class:`SessionMixin` interface. 

269 

270 This will return ``None`` to indicate that loading failed in 

271 some way that is not immediately an error. The request 

272 context will fall back to using :meth:`make_null_session` 

273 in this case. 

274 """ 

275 raise NotImplementedError() 

276 

277 def save_session( 

278 self, app: Flask, session: SessionMixin, response: Response 

279 ) -> None: 

280 """This is called at the end of each request, after generating 

281 a response, before removing the request context. It is skipped 

282 if :meth:`is_null_session` returns ``True``. 

283 """ 

284 raise NotImplementedError() 

285 

286 

287session_json_serializer = TaggedJSONSerializer() 

288 

289 

290def _lazy_sha1(string: bytes = b"") -> t.Any: 

291 """Don't access ``hashlib.sha1`` until runtime. FIPS builds may not include 

292 SHA-1, in which case the import and use as a default would fail before the 

293 developer can configure something else. 

294 """ 

295 return hashlib.sha1(string) 

296 

297 

298class SecureCookieSessionInterface(SessionInterface): 

299 """The default session interface that stores sessions in signed cookies 

300 through the :mod:`itsdangerous` module. 

301 """ 

302 

303 #: the salt that should be applied on top of the secret key for the 

304 #: signing of cookie based sessions. 

305 salt = "cookie-session" 

306 #: the hash function to use for the signature. The default is sha1 

307 digest_method = staticmethod(_lazy_sha1) 

308 #: the name of the itsdangerous supported key derivation. The default 

309 #: is hmac. 

310 key_derivation = "hmac" 

311 #: A python serializer for the payload. The default is a compact 

312 #: JSON derived serializer with support for some extra Python types 

313 #: such as datetime objects or tuples. 

314 serializer = session_json_serializer 

315 session_class = SecureCookieSession 

316 

317 def get_signing_serializer(self, app: Flask) -> URLSafeTimedSerializer | None: 

318 if not app.secret_key: 

319 return None 

320 

321 keys: list[str | bytes] = [] 

322 

323 if fallbacks := app.config["SECRET_KEY_FALLBACKS"]: 

324 keys.extend(fallbacks) 

325 

326 keys.append(app.secret_key) # itsdangerous expects current key at top 

327 return URLSafeTimedSerializer( 

328 keys, # type: ignore[arg-type] 

329 salt=self.salt, 

330 serializer=self.serializer, 

331 signer_kwargs={ 

332 "key_derivation": self.key_derivation, 

333 "digest_method": self.digest_method, 

334 }, 

335 ) 

336 

337 def open_session(self, app: Flask, request: Request) -> SecureCookieSession | None: 

338 s = self.get_signing_serializer(app) 

339 if s is None: 

340 return None 

341 val = request.cookies.get(self.get_cookie_name(app)) 

342 if not val: 

343 return self.session_class() 

344 max_age = int(app.permanent_session_lifetime.total_seconds()) 

345 try: 

346 data = s.loads(val, max_age=max_age) 

347 return self.session_class(data) 

348 except BadSignature: 

349 return self.session_class() 

350 

351 def save_session( 

352 self, app: Flask, session: SessionMixin, response: Response 

353 ) -> None: 

354 name = self.get_cookie_name(app) 

355 domain = self.get_cookie_domain(app) 

356 path = self.get_cookie_path(app) 

357 secure = self.get_cookie_secure(app) 

358 partitioned = self.get_cookie_partitioned(app) 

359 samesite = self.get_cookie_samesite(app) 

360 httponly = self.get_cookie_httponly(app) 

361 

362 # Add a "Vary: Cookie" header if the session was accessed at all. 

363 if session.accessed: 

364 response.vary.add("Cookie") 

365 

366 # If the session is modified to be empty, remove the cookie. 

367 # If the session is empty, return without setting the cookie. 

368 if not session: 

369 if session.modified: 

370 response.delete_cookie( 

371 name, 

372 domain=domain, 

373 path=path, 

374 secure=secure, 

375 partitioned=partitioned, 

376 samesite=samesite, 

377 httponly=httponly, 

378 ) 

379 response.vary.add("Cookie") 

380 

381 return 

382 

383 if not self.should_set_cookie(app, session): 

384 return 

385 

386 expires = self.get_expiration_time(app, session) 

387 val = self.get_signing_serializer(app).dumps(dict(session)) # type: ignore[union-attr] 

388 response.set_cookie( 

389 name, 

390 val, 

391 expires=expires, 

392 httponly=httponly, 

393 domain=domain, 

394 path=path, 

395 secure=secure, 

396 partitioned=partitioned, 

397 samesite=samesite, 

398 ) 

399 response.vary.add("Cookie")