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

115 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) # type: ignore[no-any-return] 

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 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 """ 

55 

56 

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

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

59 

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 """ 

65 

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 

72 

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 

79 

80 super().__init__(initial, on_update) 

81 

82 

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 """ 

88 

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 ) 

95 

96 __setitem__ = __delitem__ = clear = pop = popitem = update = setdefault = _fail 

97 del _fail 

98 

99 

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. 

106 

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:: 

111 

112 class Session(dict, SessionMixin): 

113 pass 

114 

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. 

120 

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

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

123 

124 app = Flask(__name__) 

125 app.session_interface = MySessionInterface() 

126 

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. 

133 

134 .. versionadded:: 0.8 

135 """ 

136 

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 

142 

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 

149 

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. 

157 

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

159 """ 

160 return self.null_session_class() 

161 

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. 

165 

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) 

170 

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] 

174 

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. 

179 

180 Uses the :data:`SESSION_COOKIE_DOMAIN` config. 

181 

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] 

186 

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] 

194 

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] 

201 

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] 

207 

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] 

214 

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`. 

218 

219 .. versionadded:: 3.1 

220 """ 

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

222 

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 

232 

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. 

239 

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

241 

242 .. versionadded:: 0.11 

243 """ 

244 

245 return session.modified or ( 

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

247 ) 

248 

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. 

252 

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

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

255 

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() 

262 

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() 

271 

272 

273session_json_serializer = TaggedJSONSerializer() 

274 

275 

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) 

282 

283 

284class SecureCookieSessionInterface(SessionInterface): 

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

286 through the :mod:`itsdangerous` module. 

287 """ 

288 

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 

302 

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

304 if not app.secret_key: 

305 return None 

306 

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

308 

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

310 keys.extend(fallbacks) 

311 

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 ) 

322 

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() 

336 

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) 

347 

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

349 if session.accessed: 

350 response.vary.add("Cookie") 

351 

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") 

366 

367 return 

368 

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

370 return 

371 

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")