From 67c771a1f4a3cc5d19583d30198425dce4525118 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 14 Apr 2026 11:35:41 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[HIGH]=20Fi?= =?UTF-8?q?x=20information=20exposure=20through=20error=20messages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added global exception handler in backend/main.py to sanitize 500 errors. - Refactored Mistral agent endpoints to remove detail=str(e). - Added comprehensive security tests in tests/test_sentinel_errors.py. - Updated security journal with findings and preventions. Co-authored-by: AGI-Corporation <186229839+AGI-Corporation@users.noreply.github.com> --- .jules/sentinel.md | 5 ++++ agents/mistral_agent/agent.py | 10 +++++--- backend/main.py | 44 ++++++++++++++++++++++++++++++++++- tests/test_sentinel_errors.py | 35 ++++++++++++++++++++++++++++ 4 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 tests/test_sentinel_errors.py diff --git a/.jules/sentinel.md b/.jules/sentinel.md index f07ab16..ef3b5ee 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -7,3 +7,8 @@ **Vulnerability:** Not a direct security vulnerability, but an environmental instability. The `requirements.txt` allowed `mistralai>=1.1.0`, which pulled in version 2.x. **Learning:** MistralAI 2.x introduces breaking changes in the client import structure (`from mistralai import Mistral` fails if not using the new client correctly or if expecting the old one). This caused the entire application (including security tests) to fail on startup. **Prevention:** Pin critical dependencies like `mistralai==1.1.0` in `requirements.txt` to ensure consistent behavior across development and CI environments, especially when using agents that rely on specific API structures. + +## 2025-05-15 - Information Exposure through Error Messages +**Vulnerability:** Raw exception details (`str(e)`) were being returned to the client in HTTP 500 responses across multiple endpoints, potentially leaking sensitive system state, file paths, or credentials. +**Learning:** Defaulting to returning exception strings in API responses is a common but dangerous practice. A centralized exception handler in FastAPI provides a robust safety net, but must be carefully implemented to avoid swallowing intentional `HTTPException` or `RequestValidationError` responses. +**Prevention:** Implement a global `@app.exception_handler(Exception)` that logs full tracebacks server-side but returns generic messages to clients. Avoid `detail=str(e)` in all production code. diff --git a/agents/mistral_agent/agent.py b/agents/mistral_agent/agent.py index e09878d..1ea204f 100644 --- a/agents/mistral_agent/agent.py +++ b/agents/mistral_agent/agent.py @@ -8,6 +8,7 @@ """ import json +import logging import os import uuid from datetime import UTC, datetime @@ -318,7 +319,8 @@ async def gap_analysis(req: GapAnalysisRequest, db: AsyncSession = Depends(get_d "model": MISTRAL_MODEL, } except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) + logging.error(f"Mistral Gap Analysis Error: {str(e)}", exc_info=True) + raise HTTPException(status_code=500, detail="Internal server error") @router.post("/code-review", summary="DevSecOps code security analysis with Codestral") @@ -333,7 +335,8 @@ async def code_review(req: CodeReviewRequest, db: AsyncSession = Depends(get_db) ) return {"analysis": result, "model": MISTRAL_CODE_MODEL} except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) + logging.error(f"Mistral Code Review Error: {str(e)}", exc_info=True) + raise HTTPException(status_code=500, detail="Internal server error") @router.post("/ask", summary="Ask a CMMC/ZT compliance question") @@ -343,4 +346,5 @@ async def ask_question(req: QuestionRequest): answer = await agent.answer_compliance_question(req.question, req.context) return {"question": req.question, "answer": answer, "model": MISTRAL_MODEL} except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) + logging.error(f"Mistral Ask Error: {str(e)}", exc_info=True) + raise HTTPException(status_code=500, detail="Internal server error") diff --git a/backend/main.py b/backend/main.py index 7c2d83b..674cac2 100644 --- a/backend/main.py +++ b/backend/main.py @@ -8,12 +8,15 @@ """ import json +import logging import os from contextlib import asynccontextmanager from dotenv import load_dotenv -from fastapi import FastAPI +from fastapi import FastAPI, HTTPException, Request +from fastapi.exceptions import RequestValidationError from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse from fastapi_mcp import FastApiMCP from agents.devsecops_agent import agent as devsecops @@ -34,6 +37,13 @@ async def lifespan(app: FastAPI): yield +# ─── Logging ─────────────────────────────────────────────────────────────────── + +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger(__name__) + # ─── FastAPI Application ─────────────────────────────────────────────────────── app = FastAPI( @@ -64,6 +74,38 @@ async def lifespan(app: FastAPI): # Add Security Headers Middleware app.add_middleware(SecurityHeadersMiddleware) + +# ─── Exception Handlers ─────────────────────────────────────────────────────── + + +@app.exception_handler(Exception) +async def global_exception_handler(request: Request, exc: Exception): + """ + Global exception handler to prevent sensitive information leakage. + Logs the full error server-side and returns a generic 500 message. + """ + # Allow FastAPI's built-in exception handlers to handle their specific cases + if isinstance(exc, HTTPException): + return JSONResponse( + status_code=exc.status_code, + content={"detail": exc.detail}, + ) + if isinstance(exc, RequestValidationError): + return JSONResponse( + status_code=422, + content={"detail": exc.errors()}, + ) + + # Log the actual exception for debugging + logger.error(f"Unhandled error: {str(exc)}", exc_info=True) + + # Return a safe, generic error message to the client + return JSONResponse( + status_code=500, + content={"detail": "Internal server error"}, + ) + + # ─── Routers ────────────────────────────────────────────────────────────────── # Core Routers diff --git a/tests/test_sentinel_errors.py b/tests/test_sentinel_errors.py new file mode 100644 index 0000000..1e3ee8f --- /dev/null +++ b/tests/test_sentinel_errors.py @@ -0,0 +1,35 @@ +import pytest +from httpx import AsyncClient, ASGITransport +from backend.main import app + +@pytest.mark.asyncio +async def test_error_leakage(): + # Adding a temporary route to trigger an error + @app.get("/api/test-error-leakage") + async def trigger_error(): + raise ValueError("Sensitive database connection string: postgresql://user:password@localhost:5432/db") + + async with AsyncClient(transport=ASGITransport(app=app, raise_app_exceptions=False), base_url="http://test") as ac: + response = await ac.get("/api/test-error-leakage") + + assert response.status_code == 500 + assert "Sensitive database connection string" not in response.text + assert "password" not in response.text + assert "Internal server error" in response.text + +@pytest.mark.asyncio +async def test_http_exception_passthrough(): + async with AsyncClient(transport=ASGITransport(app=app, raise_app_exceptions=False), base_url="http://test") as ac: + response = await ac.get("/api/controls/NONEXISTENT") + + assert response.status_code == 404 + assert "Control NONEXISTENT not found" in response.text + +@pytest.mark.asyncio +async def test_validation_error_passthrough(): + async with AsyncClient(transport=ASGITransport(app=app, raise_app_exceptions=False), base_url="http://test") as ac: + # Sending invalid data to a POST endpoint + response = await ac.post("/api/agents/mistral/gap-analysis", json={"invalid": "data"}) + + assert response.status_code == 422 + assert "detail" in response.json()