From 92444f9878f67473de747507f8221dec017bfbe6 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 22:59:26 +0000 Subject: [PATCH] Fix error disclosure and enforce JWT secret - Remove 'details': str(e) from login_required to prevent leaking exception details to unauthenticated callers. - Require JWT_SECRET_KEY environment variable in JWTUtils rather than falling back to a hardcoded default secret. - Update deprecated datetime.utcnow() usages to datetime.now(timezone.utc). - Add unit tests in tests/test_auth_utils.py. Co-authored-by: Pmaster-dev <293764797+Pmaster-dev@users.noreply.github.com> --- auth/utils.py | 30 +++++++++------ tests/test_auth_utils.py | 81 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 11 deletions(-) create mode 100644 tests/test_auth_utils.py diff --git a/auth/utils.py b/auth/utils.py index 4bded89..9bbd76a 100644 --- a/auth/utils.py +++ b/auth/utils.py @@ -2,7 +2,7 @@ import jwt import os import secrets -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from functools import wraps from flask import request, jsonify, g from flask_jwt_extended import get_jwt_identity, verify_jwt_in_request @@ -34,17 +34,25 @@ def verify_password(password: str, hash_: str) -> bool: class JWTUtils: """JWT token utilities""" + @staticmethod + def _get_secret_key() -> str: + secret = os.getenv('JWT_SECRET_KEY') + if not secret: + raise ValueError("JWT_SECRET_KEY environment variable is not set") + return secret + @staticmethod def create_tokens(user_id: str, username: str) -> Tuple[str, str]: """Create access and refresh tokens""" + secret_key = JWTUtils._get_secret_key() access_token = jwt.encode( { 'user_id': user_id, 'username': username, - 'exp': datetime.utcnow() + timedelta(hours=1), + 'exp': datetime.now(timezone.utc) + timedelta(hours=1), 'type': 'access' }, - os.getenv('JWT_SECRET_KEY', 'jwt-secret'), + secret_key, algorithm='HS256' ) @@ -52,10 +60,10 @@ def create_tokens(user_id: str, username: str) -> Tuple[str, str]: { 'user_id': user_id, 'username': username, - 'exp': datetime.utcnow() + timedelta(days=30), + 'exp': datetime.now(timezone.utc) + timedelta(days=30), 'type': 'refresh' }, - os.getenv('JWT_SECRET_KEY', 'jwt-secret'), + secret_key, algorithm='HS256' ) @@ -65,15 +73,14 @@ def create_tokens(user_id: str, username: str) -> Tuple[str, str]: def decode_token(token: str) -> Optional[dict]: """Decode and verify token""" try: + secret_key = JWTUtils._get_secret_key() payload = jwt.decode( token, - os.getenv('JWT_SECRET_KEY', 'jwt-secret'), + secret_key, algorithms=['HS256'] ) return payload - except jwt.ExpiredSignatureError: - return None - except jwt.InvalidTokenError: + except (ValueError, jwt.ExpiredSignatureError, jwt.InvalidTokenError): return None @@ -128,8 +135,9 @@ def decorated_function(*args, **kwargs): g.user_id = user_id g.user = user_data return f(*args, **kwargs) - except Exception as e: - return jsonify({'error': 'Unauthorized', 'details': str(e)}), 401 + except Exception: + # Do not leak internal exception details to unauthenticated callers + return jsonify({'error': 'Unauthorized'}), 401 return decorated_function diff --git a/tests/test_auth_utils.py b/tests/test_auth_utils.py new file mode 100644 index 0000000..f09b10f --- /dev/null +++ b/tests/test_auth_utils.py @@ -0,0 +1,81 @@ +import sys +from unittest.mock import MagicMock + +# Mock external cache_db module before importing auth.utils +mock_cache_db = MagicMock() +mock_redis_client = MagicMock() +mock_models = MagicMock() +sys.modules["cache_db"] = mock_cache_db +sys.modules["cache_db.redis_client"] = mock_cache_db +mock_cache_db.redis_client = mock_redis_client +sys.modules["cache_db.models"] = mock_models +mock_models.User = MagicMock() +mock_models.RefreshToken = MagicMock() + +import os +import pytest +from flask import Flask +from auth.utils import ( + PasswordUtils, + JWTUtils, + SessionUtils, + login_required, + admin_required, + verify_required, +) + + +def test_password_utils(): + hashed = PasswordUtils.hash_password("securepassword123") + assert PasswordUtils.verify_password("securepassword123", hashed) is True + assert PasswordUtils.verify_password("wrongpassword", hashed) is False + + with pytest.raises(ValueError): + PasswordUtils.hash_password("short") + + +def test_jwt_utils_missing_secret(monkeypatch): + monkeypatch.delenv("JWT_SECRET_KEY", raising=False) + with pytest.raises(ValueError, match="JWT_SECRET_KEY environment variable is not set"): + JWTUtils.create_tokens("user123", "alice") + + assert JWTUtils.decode_token("some.jwt.token") is None + + +def test_jwt_utils_with_secret(monkeypatch): + monkeypatch.setenv("JWT_SECRET_KEY", "super-secret-key-12345") + access_token, refresh_token = JWTUtils.create_tokens("user123", "alice") + assert access_token is not None + assert refresh_token is not None + + decoded = JWTUtils.decode_token(access_token) + assert decoded is not None + assert decoded["user_id"] == "user123" + assert decoded["username"] == "alice" + assert decoded["type"] == "access" + + +def test_login_required_does_not_leak_error_details(monkeypatch): + app = Flask(__name__) + app.config["TESTING"] = True + + @app.route("/protected") + @login_required + def protected_route(): + return "success" + + client = app.test_client() + + # Mock verify_jwt_in_request to raise an exception with internal sensitive info + def raise_sensitive_error(): + raise RuntimeError("Sensitive DB / JWT Exception Internal Stack Trace Details") + + monkeypatch.setattr("auth.utils.verify_jwt_in_request", raise_sensitive_error) + + response = client.get("/protected") + assert response.status_code == 401 + json_data = response.get_json() + assert json_data == {"error": "Unauthorized"} + # Ensure details field is not present and no stack trace is leaked + assert "details" not in json_data + assert "Sensitive DB" not in response.get_data(as_text=True)