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

135 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-07 06:35 +0000

1import hashlib 

2import typing as t 

3import warnings 

4from collections.abc import MutableMapping 

5from datetime import datetime 

6from datetime import timezone 

7 

8from itsdangerous import BadSignature 

9from itsdangerous import URLSafeTimedSerializer 

10from werkzeug.datastructures import CallbackDict 

11 

12from .helpers import is_ip 

13from .json.tag import TaggedJSONSerializer 

14 

15if t.TYPE_CHECKING: # pragma: no cover 

16 import typing_extensions as te 

17 from .app import Flask 

18 from .wrappers import Request, Response 

19 

20 

21class SessionMixin(MutableMapping): 

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

23 

24 @property 

25 def permanent(self) -> bool: 

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

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

28 

29 @permanent.setter 

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

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

32 

33 #: Some implementations can detect whether a session is newly 

34 #: created, but that is not guaranteed. Use with caution. The mixin 

35 # default is hard-coded ``False``. 

36 new = False 

37 

38 #: Some implementations can detect changes to the session and set 

39 #: this when that happens. The mixin default is hard coded to 

40 #: ``True``. 

41 modified = True 

42 

43 #: Some implementations can detect when session data is read or 

44 #: written and set this when that happens. The mixin default is hard 

45 #: coded to ``True``. 

46 accessed = True 

47 

48 

49class SecureCookieSession(CallbackDict, SessionMixin): 

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

51 

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

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

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

55 ``False``. 

56 """ 

57 

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

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

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

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

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

63 modified = False 

64 

65 #: When data is read or written, this is set to ``True``. Used by 

66 # :class:`.SecureCookieSessionInterface` to add a ``Vary: Cookie`` 

67 #: header, which allows caching proxies to cache different pages for 

68 #: different users. 

69 accessed = False 

70 

71 def __init__(self, initial: t.Any = None) -> None: 

72 def on_update(self) -> None: 

73 self.modified = True 

74 self.accessed = True 

75 

76 super().__init__(initial, on_update) 

77 

78 def __getitem__(self, key: str) -> t.Any: 

79 self.accessed = True 

80 return super().__getitem__(key) 

81 

82 def get(self, key: str, default: t.Any = None) -> t.Any: 

83 self.accessed = True 

84 return super().get(key, default) 

85 

86 def setdefault(self, key: str, default: t.Any = None) -> t.Any: 

87 self.accessed = True 

88 return super().setdefault(key, default) 

89 

90 

91class NullSession(SecureCookieSession): 

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

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

94 but fail on setting. 

95 """ 

96 

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

98 raise RuntimeError( 

99 "The session is unavailable because no secret " 

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

101 "application to something unique and secret." 

102 ) 

103 

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

105 del _fail 

106 

107 

108class SessionInterface: 

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

110 default session interface which uses werkzeug's securecookie 

111 implementation. The only methods you have to implement are 

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

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

114 

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

116 provide a dictionary like interface plus the properties and methods 

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

118 and adding that mixin:: 

119 

120 class Session(dict, SessionMixin): 

121 pass 

122 

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

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

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

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

127 will complain that the secret key was not set. 

128 

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

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

131 

132 app = Flask(__name__) 

133 app.session_interface = MySessionInterface() 

134 

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

136 concurrently. When implementing a new session interface, consider 

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

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

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

140 begin and end processing. 

141 

142 .. versionadded:: 0.8 

143 """ 

144 

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

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

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

148 #: this type. 

149 null_session_class = NullSession 

150 

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

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

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

154 #: 

155 #: .. versionadded:: 0.10 

156 pickle_based = False 

157 

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

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

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

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

162 null session is to still support lookup without complaining but 

163 modifications are answered with a helpful error message of what 

164 failed. 

165 

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

167 """ 

168 return self.null_session_class() 

169 

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

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

172 not asked to be saved. 

173 

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

175 by default. 

176 """ 

177 return isinstance(obj, self.null_session_class) 

178 

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

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

181 return app.config["SESSION_COOKIE_NAME"] 

182 

183 def get_cookie_domain(self, app: "Flask") -> t.Optional[str]: 

184 """Returns the domain that should be set for the session cookie. 

185 

186 Uses ``SESSION_COOKIE_DOMAIN`` if it is configured, otherwise 

187 falls back to detecting the domain based on ``SERVER_NAME``. 

188 

189 Once detected (or if not set at all), ``SESSION_COOKIE_DOMAIN`` is 

190 updated to avoid re-running the logic. 

191 """ 

192 

193 rv = app.config["SESSION_COOKIE_DOMAIN"] 

194 

195 # set explicitly, or cached from SERVER_NAME detection 

196 # if False, return None 

197 if rv is not None: 

198 return rv if rv else None 

199 

200 rv = app.config["SERVER_NAME"] 

201 

202 # server name not set, cache False to return none next time 

203 if not rv: 

204 app.config["SESSION_COOKIE_DOMAIN"] = False 

205 return None 

206 

207 # chop off the port which is usually not supported by browsers 

208 # remove any leading '.' since we'll add that later 

209 rv = rv.rsplit(":", 1)[0].lstrip(".") 

210 

211 if "." not in rv: 

212 # Chrome doesn't allow names without a '.'. This should only 

213 # come up with localhost. Hack around this by not setting 

214 # the name, and show a warning. 

215 warnings.warn( 

216 f"{rv!r} is not a valid cookie domain, it must contain" 

217 " a '.'. Add an entry to your hosts file, for example" 

218 f" '{rv}.localdomain', and use that instead." 

219 ) 

220 app.config["SESSION_COOKIE_DOMAIN"] = False 

221 return None 

222 

223 ip = is_ip(rv) 

224 

225 if ip: 

226 warnings.warn( 

227 "The session cookie domain is an IP address. This may not work" 

228 " as intended in some browsers. Add an entry to your hosts" 

229 ' file, for example "localhost.localdomain", and use that' 

230 " instead." 

231 ) 

232 

233 # if this is not an ip and app is mounted at the root, allow subdomain 

234 # matching by adding a '.' prefix 

235 if self.get_cookie_path(app) == "/" and not ip: 

236 rv = f".{rv}" 

237 

238 app.config["SESSION_COOKIE_DOMAIN"] = rv 

239 return rv 

240 

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

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

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

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

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

246 """ 

247 return app.config["SESSION_COOKIE_PATH"] or app.config["APPLICATION_ROOT"] 

248 

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

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

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

252 config var. 

253 """ 

254 return app.config["SESSION_COOKIE_HTTPONLY"] 

255 

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

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

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

259 """ 

260 return app.config["SESSION_COOKIE_SECURE"] 

261 

262 def get_cookie_samesite(self, app: "Flask") -> str: 

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

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

265 the :data:`SESSION_COOKIE_SAMESITE` setting. 

266 """ 

267 return app.config["SESSION_COOKIE_SAMESITE"] 

268 

269 def get_expiration_time( 

270 self, app: "Flask", session: SessionMixin 

271 ) -> t.Optional[datetime]: 

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

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

274 default implementation returns now + the permanent session 

275 lifetime configured on the application. 

276 """ 

277 if session.permanent: 

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

279 return None 

280 

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

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

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

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

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

286 always set. 

287 

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

289 

290 .. versionadded:: 0.11 

291 """ 

292 

293 return session.modified or ( 

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

295 ) 

296 

297 def open_session( 

298 self, app: "Flask", request: "Request" 

299 ) -> t.Optional[SessionMixin]: 

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

301 pushing the request context, before matching the URL. 

302 

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

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

305 

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

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

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

309 in this case. 

310 """ 

311 raise NotImplementedError() 

312 

313 def save_session( 

314 self, app: "Flask", session: SessionMixin, response: "Response" 

315 ) -> None: 

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

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

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

319 """ 

320 raise NotImplementedError() 

321 

322 

323session_json_serializer = TaggedJSONSerializer() 

324 

325 

326class SecureCookieSessionInterface(SessionInterface): 

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

328 through the :mod:`itsdangerous` module. 

329 """ 

330 

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

332 #: signing of cookie based sessions. 

333 salt = "cookie-session" 

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

335 digest_method = staticmethod(hashlib.sha1) 

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

337 #: is hmac. 

338 key_derivation = "hmac" 

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

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

341 #: such as datetime objects or tuples. 

342 serializer = session_json_serializer 

343 session_class = SecureCookieSession 

344 

345 def get_signing_serializer( 

346 self, app: "Flask" 

347 ) -> t.Optional[URLSafeTimedSerializer]: 

348 if not app.secret_key: 

349 return None 

350 signer_kwargs = dict( 

351 key_derivation=self.key_derivation, digest_method=self.digest_method 

352 ) 

353 return URLSafeTimedSerializer( 

354 app.secret_key, 

355 salt=self.salt, 

356 serializer=self.serializer, 

357 signer_kwargs=signer_kwargs, 

358 ) 

359 

360 def open_session( 

361 self, app: "Flask", request: "Request" 

362 ) -> t.Optional[SecureCookieSession]: 

363 s = self.get_signing_serializer(app) 

364 if s is None: 

365 return None 

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

367 if not val: 

368 return self.session_class() 

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

370 try: 

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

372 return self.session_class(data) 

373 except BadSignature: 

374 return self.session_class() 

375 

376 def save_session( 

377 self, app: "Flask", session: SessionMixin, response: "Response" 

378 ) -> None: 

379 name = self.get_cookie_name(app) 

380 domain = self.get_cookie_domain(app) 

381 path = self.get_cookie_path(app) 

382 secure = self.get_cookie_secure(app) 

383 samesite = self.get_cookie_samesite(app) 

384 httponly = self.get_cookie_httponly(app) 

385 

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

387 if session.accessed: 

388 response.vary.add("Cookie") 

389 

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

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

392 if not session: 

393 if session.modified: 

394 response.delete_cookie( 

395 name, 

396 domain=domain, 

397 path=path, 

398 secure=secure, 

399 samesite=samesite, 

400 httponly=httponly, 

401 ) 

402 response.vary.add("Cookie") 

403 

404 return 

405 

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

407 return 

408 

409 expires = self.get_expiration_time(app, session) 

410 val = self.get_signing_serializer(app).dumps(dict(session)) # type: ignore 

411 response.set_cookie( 

412 name, 

413 val, # type: ignore 

414 expires=expires, 

415 httponly=httponly, 

416 domain=domain, 

417 path=path, 

418 secure=secure, 

419 samesite=samesite, 

420 ) 

421 response.vary.add("Cookie")