From 8ffa65a9da782a4cc5058f8c55bbe93443207390 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Tue, 18 Aug 2026 02:47:05 +0200 Subject: [PATCH] fix(security): bump cryptography 46.0.7 -> 50.0.0 to clear 4 CVEs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- backend/requirements.lock | 2 +- backend/requirements.txt | 2 +- backend/tests/test_security.py | 36 ++++++++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 backend/tests/test_security.py diff --git a/backend/requirements.lock b/backend/requirements.lock index 3b8d8188..8fc9ef93 100644 --- a/backend/requirements.lock +++ b/backend/requirements.lock @@ -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 diff --git a/backend/requirements.txt b/backend/requirements.txt index 2a8cf384..3112eff1 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -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 diff --git a/backend/tests/test_security.py b/backend/tests/test_security.py new file mode 100644 index 00000000..133c88f8 --- /dev/null +++ b/backend/tests/test_security.py @@ -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