Retrieve a managed session by session-id, verifying integrity and decrypting the body. File format: +-------------------------------------------------------+ | _HMAC_HEX_LEN bytes : hex HMAC over the ciphertext | +-------------------------------
(self, sid, digest)
| 325 | return ManagedSession(sid=sid) |
| 326 | |
| 327 | def get(self, sid, digest): |
| 328 | """Retrieve a managed session by session-id, verifying integrity |
| 329 | and decrypting the body. |
| 330 | |
| 331 | File format: |
| 332 | +-------------------------------------------------------+ |
| 333 | | _HMAC_HEX_LEN bytes : hex HMAC over the ciphertext | |
| 334 | +-------------------------------------------------------+ |
| 335 | | N bytes : Fernet(pickle((randval, digest, data))) | |
| 336 | +-------------------------------------------------------+ |
| 337 | |
| 338 | The HMAC is verified against the ciphertext BEFORE Fernet decrypt |
| 339 | and BEFORE pickle.loads, so a malicious file dropped in the |
| 340 | sessions directory cannot trigger arbitrary code execution via |
| 341 | __reduce__ during deserialization (encrypt-then-MAC; pickle.loads |
| 342 | only ever sees Fernet-decrypted plaintext from a server-written |
| 343 | file). |
| 344 | """ |
| 345 | fname = safe_join(self.path, sid) |
| 346 | if fname is None or not os.path.exists(fname): |
| 347 | return self.new_session() |
| 348 | |
| 349 | try: |
| 350 | with open(fname, 'rb') as f: |
| 351 | header = f.read(_HMAC_HEX_LEN) |
| 352 | body = f.read() |
| 353 | if len(header) != _HMAC_HEX_LEN or len(body) == 0: |
| 354 | # 0-byte placeholder from new_session(), or pre-fix legacy |
| 355 | # file shorter than the header. Silently invalidate; one-time |
| 356 | # re-login on upgrade is acceptable and expected. |
| 357 | return self.new_session() |
| 358 | expected = _compute_file_hmac(self.secret, body) |
| 359 | if not hmac.compare_digest(header, expected): |
| 360 | logger.warning( |
| 361 | "session file rejected: bad file-HMAC for sid=%s", |
| 362 | sid[:8] if sid else '<empty>', |
| 363 | ) |
| 364 | return self.new_session() |
| 365 | # Body integrity verified — decrypt before deserializing. |
| 366 | try: |
| 367 | plaintext = _derive_session_fernet( |
| 368 | self.secret).decrypt(body) |
| 369 | except InvalidToken: |
| 370 | # Pre-Layer-1 (HMAC-only) session files: HMAC is still a |
| 371 | # valid HMAC over the legacy plain-pickle body, but |
| 372 | # Fernet.decrypt on raw pickle bytes raises InvalidToken. |
| 373 | # Silently invalidate; users see a one-time re-login on |
| 374 | # the upgrade that introduced this layer. |
| 375 | logger.warning( |
| 376 | "session file rejected: legacy unencrypted body for " |
| 377 | "sid=%s", sid[:8] if sid else '<empty>', |
| 378 | ) |
| 379 | return self.new_session() |
| 380 | randval, hmac_digest, data = pickle.loads(plaintext) |
| 381 | except (pickle.UnpicklingError, EOFError, OSError, MemoryError, |
| 382 | ValueError, TypeError): |
| 383 | # Narrow catch: silently invalidate corrupted/unreadable files. |
| 384 | # Programming errors (AttributeError, NameError, etc.) propagate |