From cc872a979a597172862cea5beba950fe33a3ea72 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 9 Sep 2026 22:56:38 +0000 Subject: [PATCH] Fix information leakage in login_required error response Omit raw exception details from the JSON error payload in login_required to avoid leaking internal system details or credentials to untrusted clients. Exception details are now safely logged server-side. Co-authored-by: Pmaster-dev <293764797+Pmaster-dev@users.noreply.github.com> --- .jules/sentinel.md | 4 ++++ auth/utils.py | 15 ++++++++++----- src/handoff/__init__.py | 0 tests/test_auth_utils.py | 40 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 54 insertions(+), 5 deletions(-) create mode 100644 .jules/sentinel.md create mode 100644 src/handoff/__init__.py create mode 100644 tests/test_auth_utils.py 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()