From e69e4681b0b3da870592bc08d4a31ac8d2598b88 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 22:56:59 +0000 Subject: [PATCH] Fix error detail leakage in login_required decorator Prevent internal exception details from being returned to unauthenticated clients in auth/utils.py's login_required decorator. Log authentication failures internally and return generic error responses. Add unit test coverage for auth utilities. Co-authored-by: Pmaster-dev <293764797+Pmaster-dev@users.noreply.github.com> --- .jules/sentinel.md | 4 ++++ auth/utils.py | 13 +++++++---- src/handoff/__init__.py | 0 tests/test_auth_utils.py | 49 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 62 insertions(+), 4 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..3fe6a41 --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,4 @@ +## 2025-02-23 - Prevent Exception Details Leakage in Auth Responses +**Vulnerability:** `auth/utils.py`'s `@login_required` decorator exposed internal exception strings (`details: str(e)`) to clients upon authentication failure, risking exposure of database/cache state or internal implementation details. +**Learning:** Returning `str(e)` in Flask error responses provides an easy path for information disclosure when third-party libraries or DB connections fail during auth verification. +**Prevention:** Always log exception details internally via standard logging (`logger.warning` / `logger.error`) and return sanitized generic error responses (e.g. `{'error': 'Unauthorized'}`) to clients. diff --git a/auth/utils.py b/auth/utils.py index 4bded89..34776ce 100644 --- a/auth/utils.py +++ b/auth/utils.py @@ -1,14 +1,17 @@ -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: @@ -129,7 +132,9 @@ def decorated_function(*args, **kwargs): g.user = user_data return f(*args, **kwargs) except Exception as e: - return jsonify({'error': 'Unauthorized', 'details': str(e)}), 401 + # SECURITY: Do not leak internal exception details to client + logger.warning("Authentication failed: %s", e) + 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..07eac20 --- /dev/null +++ b/tests/test_auth_utils.py @@ -0,0 +1,49 @@ +import sys +from unittest.mock import MagicMock + +# Mock external cache_db module before importing auth.utils +cache_db_mock = MagicMock() +sys.modules['cache_db'] = cache_db_mock +sys.modules['cache_db.redis_client'] = cache_db_mock +sys.modules['cache_db.models'] = cache_db_mock + +from flask import Flask +from auth.utils import PasswordUtils, JWTUtils, login_required + + +def test_password_hashing(): + pw = "secret_password123" + hashed = PasswordUtils.hash_password(pw) + assert PasswordUtils.verify_password(pw, hashed) is True + assert PasswordUtils.verify_password("wrong_password", hashed) is False + + +def test_jwt_utils(): + access, refresh = JWTUtils.create_tokens("u123", "alice") + decoded_access = JWTUtils.decode_token(access) + assert decoded_access["user_id"] == "u123" + assert decoded_access["username"] == "alice" + assert decoded_access["type"] == "access" + + +def test_login_required_does_not_leak_details(monkeypatch): + app = Flask(__name__) + + @app.route("/protected") + @login_required + def protected(): + return "success", 200 + + def mock_verify(): + raise RuntimeError("Sensitive DB exception with stack trace or internal connection details") + + import flask_jwt_extended + monkeypatch.setattr(flask_jwt_extended, "verify_jwt_in_request", mock_verify) + + client = app.test_client() + response = client.get("/protected") + + assert response.status_code == 401 + data = response.get_json() + assert data == {"error": "Unauthorized"} + assert "details" not in data