From 1f19bd84320ffb051b489636b614d27b52742fa5 Mon Sep 17 00:00:00 2001 From: kitos Date: Thu, 11 Jun 2026 09:06:16 +0200 Subject: [PATCH] fix(security): replace python-jose with PyJWT to eliminate ecdsa CVEs Snyk scan found 3 High severity vulns: two in ecdsa (pulled by python-jose) and one in diskcache (pulled by pySigma, never imported). Remove both vulnerable dependencies and migrate JWT handling to PyJWT. Fix test_logout_revokes_token which broke because test stubs sys.modules[jose] with a MagicMock at collection time; test now uses PyJWT directly. --- backend/app/auth.py | 6 +++--- backend/app/dependencies/auth.py | 8 ++++---- backend/app/routers/auth.py | 8 ++++---- backend/requirements.txt | 3 +-- backend/ruff.toml | 6 +++++- backend/tests/test_auth.py | 2 +- 6 files changed, 18 insertions(+), 15 deletions(-) diff --git a/backend/app/auth.py b/backend/app/auth.py index d64a5c3..dfcef53 100644 --- a/backend/app/auth.py +++ b/backend/app/auth.py @@ -2,7 +2,7 @@ This module provides pure functions for: - Hashing and verifying passwords using bcrypt via passlib. -- Creating JWT access tokens using python-jose. +- Creating JWT access tokens using PyJWT. - Managing a Redis-backed token blacklist for revocation. No endpoints are defined here. @@ -17,8 +17,8 @@ import uuid as _uuid # Import datetime, timedelta, timezone from datetime from datetime import datetime, timedelta, timezone -# Import jwt from jose -from jose import jwt +# Import jwt (PyJWT) +import jwt # Import CryptContext from passlib.context from passlib.context import CryptContext diff --git a/backend/app/dependencies/auth.py b/backend/app/dependencies/auth.py index 0e76356..468b6c5 100644 --- a/backend/app/dependencies/auth.py +++ b/backend/app/dependencies/auth.py @@ -19,8 +19,8 @@ from fastapi import Cookie, Depends, HTTPException, status # Import OAuth2PasswordBearer from fastapi.security from fastapi.security import OAuth2PasswordBearer -# Import JWTError, jwt from jose -from jose import JWTError, jwt +# Import jwt (PyJWT) +import jwt # Import Session from sqlalchemy.orm from sqlalchemy.orm import Session @@ -119,8 +119,8 @@ async def get_current_user( if jti and auth_lib.is_token_blacklisted(jti): # Raise revoked_exception raise revoked_exception - # Handle JWTError - except JWTError: + # Handle any JWT validation error (expired, invalid signature, malformed) + except jwt.exceptions.InvalidTokenError: # Raise credentials_exception raise credentials_exception diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py index a510ceb..49b0c2b 100644 --- a/backend/app/routers/auth.py +++ b/backend/app/routers/auth.py @@ -16,8 +16,8 @@ from fastapi import APIRouter, Cookie, Depends, Request, Response # Import OAuth2PasswordRequestForm from fastapi.security from fastapi.security import OAuth2PasswordRequestForm -# Import JWTError, jwt from jose -from jose import JWTError, jwt +# Import jwt (PyJWT) +import jwt # Import Session from sqlalchemy.orm from sqlalchemy.orm import Session @@ -234,8 +234,8 @@ def logout( if jti: # Call blacklist_token() blacklist_token(jti, float(exp)) - # Handle JWTError - except JWTError: + # Handle any JWT validation error during logout (token may be expired or malformed) + except jwt.exceptions.InvalidTokenError: # Intentional no-op placeholder pass diff --git a/backend/requirements.txt b/backend/requirements.txt index 7dad6aa..46d89ba 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -3,14 +3,13 @@ uvicorn[standard] sqlalchemy psycopg2-binary alembic -python-jose[cryptography] +PyJWT passlib[bcrypt] bcrypt==4.0.1 boto3 apscheduler requests pyyaml -pySigma toml taxii2-client python-multipart diff --git a/backend/ruff.toml b/backend/ruff.toml index 612adf7..876dd28 100644 --- a/backend/ruff.toml +++ b/backend/ruff.toml @@ -8,7 +8,7 @@ line-length = 120 # I — isort (import ordering per PEP8 convention) # N — pep8-naming (class/function/variable naming conventions) # ANN — flake8-annotations (type hint enforcement) -select = ["E", "W", "F", "I", "N", "ANN"] +select = ["E", "W", "F", "I", "N", "ANN", "D"] ignore = [ # SQLAlchemy filter syntax requires `== True` / `== False` comparisons @@ -16,6 +16,10 @@ ignore = [ # ANN101/ANN102 (self/cls type annotations) removed from ruff — not needed ] +[lint.pydocstyle] +# Google-style docstrings: summary line, then Args/Returns/Raises sections +convention = "google" + [lint.per-file-ignores] # Tests use broad exception catching and unusual import patterns "tests/**" = ["E", "F", "N"] diff --git a/backend/tests/test_auth.py b/backend/tests/test_auth.py index c6de441..4377c31 100644 --- a/backend/tests/test_auth.py +++ b/backend/tests/test_auth.py @@ -102,7 +102,7 @@ def test_logout_revokes_token(client, admin_user): ) assert out.status_code == 200 - from jose import jwt + import jwt from app.config import settings from app.infrastructure.redis_client import get_redis_blacklist