-
Notifications
You must be signed in to change notification settings - Fork 0
Description
Summary
- Context: The
ChatControllerhandles streaming chat responses and returns error events when the LLM provider or downstream services fail. - Bug: The
ChatControllerexposes sensitive downstream response details (headers and bodies) to the end-user by including them in the SSE error event's diagnostic payload. - Actual vs. expected: Downstream headers and response bodies from exceptions like
WebClientResponseExceptionandOpenAIServiceExceptionare sent directly to the client; these should be redacted or omitted to prevent information leakage. - Impact: Potential exposure of sensitive internal metadata, downstream service details, or even authentication tokens to end-users.
Code with bug
// src/main/java/com/williamcallahan/javachat/web/ChatController.java
.onErrorResume(error -> {
String errorDetail = buildUserFacingErrorMessage(error);
String diagnostics =
error instanceof Exception exception ? describeException(exception) : error.toString(); // <-- BUG 🔴 [Exposes full exception details including headers and bodies]
PIPELINE_LOG.error(
"[{}] STREAMING ERROR (sessionId={}, exceptionType={})",
requestToken,
sessionId,
error.getClass().getSimpleName(),
error);
boolean retryable = openAIStreamingService.isRecoverableStreamingFailure(error);
return sseSupport.streamErrorEvent(errorDetail, diagnostics, retryable);
});Evidence
The describeException method in ExceptionResponseBuilder (called by ChatController via BaseController) explicitly appends response bodies and headers to the error description:
// src/main/java/com/williamcallahan/javachat/web/ExceptionResponseBuilder.java
private void appendWebClientDetails(StringBuilder details, WebClientResponseException exception) {
details.append(" [httpStatus=").append(exception.getStatusCode().value());
// ...
String responseBody = exception.getResponseBodyAsString();
if (!responseBody.isBlank()) {
details.append(", body=").append(responseBody); // <-- BUG 🔴 [Leaks downstream response body]
}
HttpHeaders headers = exception.getHeaders();
if (!headers.isEmpty()) {
details.append(", headers=").append(headers); // <-- BUG 🔴 [Leaks all response headers]
}
details.append("]");
}This diagnostic string is then passed to sseSupport.streamErrorEvent, which wraps it in a JSON payload and sends it over the SSE connection to the browser:
// src/main/java/com/williamcallahan/javachat/web/SseSupport.java
public Flux<ServerSentEvent<String>> streamErrorEvent(
String userFacingMessage, String diagnosticDetails, boolean retryable) {
// ...
return sseError(SseEventPayload.builder(userFacingMessage)
.details(diagnosticDetails) // <-- Diagnostic details (including leaked headers) are sent to the client
.code(statusCode)
.retryable(retryable)
.stage(STATUS_STAGE_STREAM)
.build());
}Exploit scenario
- An attacker initiates a chat session that triggers a downstream error (e.g., by sending a specially crafted prompt that causes a 400 error from a downstream service, or simply waiting for a transient service failure).
- The downstream service returns an error with sensitive headers (e.g., internal tokens, proxy IDs, or server version info) or a response body containing internal system details.
- The Java backend catches the exception and, via
ChatController, sends the full "diagnostics" string (including those headers and body) to the attacker's browser in an SSEerrorevent. - The attacker inspects the SSE stream (e.g., in the Network tab of browser dev tools) and retrieves the sensitive information.
Why has this bug gone undetected?
This bug often goes undetected because it only manifests during error conditions. Developers usually focus on the "user-facing message" and may not realize that the "technical details" or "diagnostics" payload being sent to the client contains unsafe information. In many environments, HttpHeaders.toString() might seem innocuous, but it does not reliably redact all sensitive or internal-only headers, especially when dealing with custom headers or different exception types like OpenAIServiceException which uses a standard Map.
Recommended fix
The describeException method should be modified to avoid including headers and response bodies in the user-facing diagnostic string. If such information is needed for debugging, it should be logged on the server side only.
// In src/main/java/com/williamcallahan/javachat/web/ExceptionResponseBuilder.java
private void appendWebClientDetails(StringBuilder details, WebClientResponseException exception) {
details.append(" [httpStatus=").append(exception.getStatusCode().value());
// Only include safe, high-level info for the client
String statusText = exception.getStatusText();
if (!statusText.isBlank()) {
details.append(" ").append(statusText);
}
// DO NOT append body or headers here // <-- FIX 🟢
details.append("]");
}Related bugs
The same ExceptionResponseBuilder is used by BaseController, affecting other controllers that might expose technical diagnostics to the UI, such as IngestionController if it were to use similar error handling for client-initiated tasks.