fix(security): bump cryptography 46.0.7 -> 50.0.0 to clear 4 CVEs
gate / frontend (push) Successful in 54s
gate / backend (push) Successful in 1m36s
gate / frontend (push) Successful in 54s
gate / backend (push) Successful in 1m36s
The pip-audit step added in E1 S4 flagged cryptography 46.0.7 for PYSEC-2026-3552 /3553/3554 and GHSA-537c-gmf6-5ccf; all are fixed by 50.0.0. The requirements.txt ceiling `<47` was blocking the upgrade — relaxed to `>=50.0.0,<51`. The lock is a surgical bump (only the cryptography line changed; cffi 2.1.1 already satisfies it, no other package moved). pip-audit -r requirements.lock is now clean. Add tests/test_security.py (there was none for the crypto primitives): the Fernet encrypt/decrypt round-trip, decrypt_optional degrading on garbage and on a key mismatch (the B-core-2 path), and the argon2 password hash/verify round-trip — so a future crypto-lib bump can't silently break the token-at-rest path. Gate green: 330 pytest.
This commit is contained in:
@@ -20,7 +20,7 @@ certifi==2026.7.22
|
||||
cffi==2.1.1
|
||||
charset-normalizer==3.5.1
|
||||
click==8.4.2
|
||||
cryptography==46.0.7
|
||||
cryptography==50.0.0
|
||||
fastapi==0.141.1
|
||||
feedparser-sgmllib==2.1.0
|
||||
feedparser==6.0.14
|
||||
|
||||
@@ -11,7 +11,7 @@ feedparser>=6.0,<7.0
|
||||
apscheduler>=3.10,<4.0
|
||||
py3langid>=0.3,<1.0
|
||||
itsdangerous>=2.1,<3.0
|
||||
cryptography>=46.0.7,<47
|
||||
cryptography>=50.0.0,<51
|
||||
argon2-cffi>=23.1,<26
|
||||
# Download center: yt-dlp does the extraction/download; ffmpeg (OS-level in the Dockerfile)
|
||||
# does the merge + postprocessing. The [default] extra pulls yt-dlp-ejs (the challenge-solver
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
"""Crypto primitives — a guard for the cryptography (Fernet) + argon2 dependency bumps.
|
||||
|
||||
Added with the cryptography 46->50 CVE bump: encrypt/decrypt/decrypt_optional had NO test, and they are
|
||||
the token-at-rest path (Google/API/SMTP/Plex secrets in system_config, OAuth refresh tokens). A fresh
|
||||
Fernet key is monkeypatched in so the test doesn't depend on TOKEN_ENCRYPTION_KEY being set in the env.
|
||||
Pure-logic (no DB).
|
||||
"""
|
||||
from cryptography.fernet import Fernet
|
||||
|
||||
from app import security
|
||||
|
||||
|
||||
def test_fernet_encrypt_decrypt_roundtrip(monkeypatch):
|
||||
monkeypatch.setattr(security, "_fernet", Fernet(Fernet.generate_key()))
|
||||
assert security.decrypt(security.encrypt("hunter2")) == "hunter2"
|
||||
assert security.encrypt(None) is None
|
||||
assert security.decrypt(None) is None
|
||||
|
||||
|
||||
def test_decrypt_optional_degrades_instead_of_raising(monkeypatch):
|
||||
# Garbage / absent input -> None (never raises).
|
||||
monkeypatch.setattr(security, "_fernet", Fernet(Fernet.generate_key()))
|
||||
assert security.decrypt_optional("not-a-valid-token") is None
|
||||
assert security.decrypt_optional(None) is None
|
||||
# And a ciphertext written under a DIFFERENT key (the TOKEN_ENCRYPTION_KEY-mismatch case that B-core-2
|
||||
# made non-fatal) also degrades to None rather than raising InvalidToken.
|
||||
ciphertext = security.encrypt("secret")
|
||||
monkeypatch.setattr(security, "_fernet", Fernet(Fernet.generate_key())) # rotate the key
|
||||
assert security.decrypt_optional(ciphertext) is None
|
||||
|
||||
|
||||
def test_password_hash_and_verify_roundtrip():
|
||||
hashed = security.hash_password("correct horse battery")
|
||||
assert security.verify_password("correct horse battery", hashed) is True
|
||||
assert security.verify_password("wrong password", hashed) is False
|
||||
assert security.verify_password("anything", None) is False
|
||||
Reference in New Issue
Block a user