diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 0000000..e215406 --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,4 @@ +## 2026-03-31 - [Prevent Information Leakage in Authentication Decorators] +**Vulnerability:** `login_required` decorator in `auth/utils.py` returned raw exception details `str(e)` in JSON responses to clients during JWT verification errors. +**Learning:** Returning exception strings in HTTP error responses can expose internal database URLs, system paths, or service credentials. +**Prevention:** Log exception details server-side using standard `logger.exception()` and return generic sanitised error payloads to clients. diff --git a/auth/utils.py b/auth/utils.py index 4bded89..50511bc 100644 --- a/auth/utils.py +++ b/auth/utils.py @@ -1,14 +1,18 @@ -import bcrypt -import jwt +import logging import os import secrets from datetime import datetime, timedelta from functools import wraps +from typing import Tuple, Optional + +import bcrypt +import jwt from flask import request, jsonify, g from flask_jwt_extended import get_jwt_identity, verify_jwt_in_request from cache_db.redis_client import redis_client from cache_db.models import User, RefreshToken -from typing import Tuple, Optional + +logger = logging.getLogger(__name__) class PasswordUtils: @@ -128,8 +132,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: + logger.exception("JWT verification failed") + return jsonify({'error': 'Unauthorized'}), 401 return decorated_function diff --git a/src/handoff/__init__.py b/src/handoff/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_auth_utils.py b/tests/test_auth_utils.py new file mode 100644 index 0000000..2c3a5f4 --- /dev/null +++ b/tests/test_auth_utils.py @@ -0,0 +1,40 @@ +import unittest +from unittest.mock import MagicMock, patch +import os +import sys + +# Mock external cache_db module before importing auth.utils +sys.modules['cache_db'] = MagicMock() +sys.modules['cache_db.redis_client'] = MagicMock() +sys.modules['cache_db.models'] = MagicMock() + +from flask import Flask, jsonify +from auth.utils import login_required + +class TestLoginRequiredSecurity(unittest.TestCase): + def setUp(self): + self.app = Flask(__name__) + self.app.config['TESTING'] = True + + @self.app.route('/protected') + @login_required + def protected_route(): + return jsonify({'message': 'success'}) + + self.client = self.app.test_client() + + @patch('auth.utils.verify_jwt_in_request') + def test_login_required_does_not_leak_exception_details(self, mock_verify): + # Simulate an exception with sensitive internal details during JWT verification + sensitive_error_msg = "Database connection string postgresql://user:secretpass@localhost/db failed" + mock_verify.side_effect = Exception(sensitive_error_msg) + + response = self.client.get('/protected') + self.assertEqual(response.status_code, 401) + data = response.get_json() + self.assertEqual(data.get('error'), 'Unauthorized') + self.assertNotIn('details', data) + self.assertNotIn('secretpass', str(data)) + +if __name__ == '__main__': + unittest.main()