mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2026-10-16 11:01:56 +00:00
fix: prevent exception diagnostics from leaking in API problem responses (#1405)
* Potential fix for code scanning alert no. 11: Information exposure through an exception traceback should not be in client side code Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * fix: keep unexpected API error details private and test problem responses --------- Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
parent
49ea062dc8
commit
812aa3dda9
@@ -1,5 +1,4 @@
|
|||||||
import html
|
import html
|
||||||
import traceback
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Optional, cast
|
from typing import Optional, cast
|
||||||
|
|
||||||
@@ -8,8 +7,6 @@ from fastapi.exceptions import HTTPException, RequestValidationError
|
|||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from akkudoktoreos.core.coreabc import get_config
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass(slots=True)
|
||||||
class EOSProblem(Exception):
|
class EOSProblem(Exception):
|
||||||
@@ -45,9 +42,6 @@ def _problem_response(
|
|||||||
if cause is not None:
|
if cause is not None:
|
||||||
logger.exception(cause)
|
logger.exception(cause)
|
||||||
|
|
||||||
if get_config().logging.api_level in ("TRACE", "DEBUG"):
|
|
||||||
body["traceback"] = traceback.format_exception(cause)
|
|
||||||
|
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
status_code=status,
|
status_code=status,
|
||||||
media_type="application/problem+json",
|
media_type="application/problem+json",
|
||||||
@@ -86,7 +80,7 @@ async def unexpected_exception_handler(request: Request, exc: Exception) -> JSON
|
|||||||
request=request,
|
request=request,
|
||||||
status=500,
|
status=500,
|
||||||
title="Internal Server Error",
|
title="Internal Server Error",
|
||||||
detail=str(exc),
|
detail="An unexpected error occurred.",
|
||||||
cause=exc,
|
cause=exc,
|
||||||
type="/problems/internal-server-error",
|
type="/problems/internal-server-error",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
"""Problem responses keep server diagnostics private and preserve public errors."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from fastapi import FastAPI, HTTPException
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from akkudoktoreos.server.rest.error import EOSProblem, register_problem_handlers
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("api_level", ["TRACE", "DEBUG", "INFO", "WARNING", "ERROR"])
|
||||||
|
@pytest.mark.parametrize("unexpected", [False, True])
|
||||||
|
def test_problem_responses_keep_diagnostics_in_logs(config_eos, api_level, unexpected):
|
||||||
|
config_eos.logging.api_level = api_level
|
||||||
|
app = FastAPI()
|
||||||
|
register_problem_handlers(app)
|
||||||
|
private_detail = "Database failed at /private/server.db with password=secret"
|
||||||
|
|
||||||
|
@app.get("/failure")
|
||||||
|
def failure():
|
||||||
|
try:
|
||||||
|
raise RuntimeError(private_detail)
|
||||||
|
except RuntimeError as exc:
|
||||||
|
if unexpected:
|
||||||
|
raise
|
||||||
|
raise EOSProblem(
|
||||||
|
status=503,
|
||||||
|
title="Service Unavailable",
|
||||||
|
detail="The service is temporarily unavailable.",
|
||||||
|
type="/problems/service-unavailable",
|
||||||
|
cause=exc,
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
logs: list[str] = []
|
||||||
|
sink_id = logger.add(logs.append, level="ERROR", backtrace=False, diagnose=False)
|
||||||
|
try:
|
||||||
|
with TestClient(app, raise_server_exceptions=False) as client:
|
||||||
|
response = client.get("/failure?token=private-query-token")
|
||||||
|
finally:
|
||||||
|
logger.remove(sink_id)
|
||||||
|
|
||||||
|
status = 500 if unexpected else 503
|
||||||
|
assert response.status_code == status
|
||||||
|
assert response.headers["content-type"] == "application/problem+json"
|
||||||
|
assert response.json() == {
|
||||||
|
"type": "/problems/internal-server-error" if unexpected else "/problems/service-unavailable",
|
||||||
|
"title": "Internal Server Error" if unexpected else "Service Unavailable",
|
||||||
|
"status": status,
|
||||||
|
"detail": (
|
||||||
|
"An unexpected error occurred."
|
||||||
|
if unexpected
|
||||||
|
else "The service is temporarily unavailable."
|
||||||
|
),
|
||||||
|
"instance": "/failure",
|
||||||
|
}
|
||||||
|
assert private_detail not in response.text
|
||||||
|
assert "private-query-token" not in response.text
|
||||||
|
assert any(private_detail in message and "Traceback" in message for message in logs)
|
||||||
|
|
||||||
|
|
||||||
|
def test_http_and_validation_errors_preserve_public_details():
|
||||||
|
app = FastAPI()
|
||||||
|
register_problem_handlers(app)
|
||||||
|
|
||||||
|
@app.get("/missing")
|
||||||
|
def missing():
|
||||||
|
raise HTTPException(status_code=404, detail="The requested item does not exist.")
|
||||||
|
|
||||||
|
@app.get("/validate")
|
||||||
|
def validate(value: int):
|
||||||
|
return {"value": value}
|
||||||
|
|
||||||
|
with TestClient(app) as client:
|
||||||
|
missing_response = client.get("/missing")
|
||||||
|
validation_response = client.get("/validate?value=private-invalid-input")
|
||||||
|
|
||||||
|
assert missing_response.status_code == 404
|
||||||
|
assert missing_response.headers["content-type"] == "application/problem+json"
|
||||||
|
assert missing_response.json() == {
|
||||||
|
"type": "about:blank",
|
||||||
|
"title": "HTTP Error",
|
||||||
|
"status": 404,
|
||||||
|
"detail": "The requested item does not exist.",
|
||||||
|
"instance": "/missing",
|
||||||
|
}
|
||||||
|
assert validation_response.status_code == 422
|
||||||
|
assert validation_response.headers["content-type"] == "application/problem+json"
|
||||||
|
assert validation_response.json() == {
|
||||||
|
"type": "/problems/validation",
|
||||||
|
"title": "Validation Error",
|
||||||
|
"status": 422,
|
||||||
|
"detail": "Request validation failed.",
|
||||||
|
"instance": "/validate",
|
||||||
|
}
|
||||||
|
assert "private-invalid-input" not in validation_response.text
|
||||||
Reference in New Issue
Block a user