From f788de30c842626e62e3a1bc3159ce2885d7ae5f Mon Sep 17 00:00:00 2001 From: David Rudduck <47308254+davidrudduck@users.noreply.github.com> Date: Mon, 9 Feb 2026 16:09:29 +1000 Subject: [PATCH] fix(security): sanitize error responses to prevent information leakage (#5) * fix(security): sanitize error responses to prevent information leakage Replace raw error messages in HTTP responses with generic messages. Internal error details (stack traces, module paths, error messages) were being returned to clients in 4 gateway endpoints. * fix: sanitize 2 additional error response leaks in openresponses-http Address CodeRabbit feedback: non-stream and streaming error paths in openresponses-http.ts were still returning String(err) to clients. * fix: add server-side error logging to sanitized catch blocks Restore err parameter and add logWarn() calls so errors are still captured server-side for diagnostics while keeping client responses sanitized. Addresses CodeRabbit feedback about silently discarded errors. --- src/gateway/openai-http.ts | 7 +++++-- src/gateway/openresponses-http.ts | 13 +++++++++---- src/gateway/tools-invoke-http.ts | 3 ++- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/gateway/openai-http.ts b/src/gateway/openai-http.ts index e85e0aac961..2b9df17cdfe 100644 --- a/src/gateway/openai-http.ts +++ b/src/gateway/openai-http.ts @@ -5,6 +5,7 @@ import { buildHistoryContextFromEntries, type HistoryEntry } from "../auto-reply import { createDefaultDeps } from "../cli/deps.js"; import { agentCommand } from "../commands/agent.js"; import { emitAgentEvent, onAgentEvent } from "../infra/agent-events.js"; +import { logWarn } from "../logger.js"; import { defaultRuntime } from "../runtime.js"; import { authorizeGatewayConnect, type ResolvedGatewayAuth } from "./auth.js"; import { @@ -264,8 +265,9 @@ export async function handleOpenAiHttpRequest( usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }, }); } catch (err) { + logWarn(`openai-compat: chat completion failed: ${String(err)}`); sendJson(res, 500, { - error: { message: String(err), type: "api_error" }, + error: { message: "internal error", type: "api_error" }, }); } return true; @@ -394,6 +396,7 @@ export async function handleOpenAiHttpRequest( }); } } catch (err) { + logWarn(`openai-compat: streaming chat completion failed: ${String(err)}`); if (closed) { return; } @@ -405,7 +408,7 @@ export async function handleOpenAiHttpRequest( choices: [ { index: 0, - delta: { content: `Error: ${String(err)}` }, + delta: { content: "Error: internal error" }, finish_reason: "stop", }, ], diff --git a/src/gateway/openresponses-http.ts b/src/gateway/openresponses-http.ts index dae20ac51d6..c4d8b9bef19 100644 --- a/src/gateway/openresponses-http.ts +++ b/src/gateway/openresponses-http.ts @@ -16,6 +16,7 @@ import { buildHistoryContextFromEntries, type HistoryEntry } from "../auto-reply import { createDefaultDeps } from "../cli/deps.js"; import { agentCommand } from "../commands/agent.js"; import { emitAgentEvent, onAgentEvent } from "../infra/agent-events.js"; +import { logWarn } from "../logger.js"; import { DEFAULT_INPUT_FILE_MAX_BYTES, DEFAULT_INPUT_FILE_MAX_CHARS, @@ -485,8 +486,9 @@ export async function handleOpenResponsesHttpRequest( } } } catch (err) { + logWarn(`openresponses: request parsing failed: ${String(err)}`); sendJson(res, 400, { - error: { message: String(err), type: "invalid_request_error" }, + error: { message: "invalid request", type: "invalid_request_error" }, }); return true; } @@ -502,8 +504,9 @@ export async function handleOpenResponsesHttpRequest( resolvedClientTools = toolChoiceResult.tools; toolChoicePrompt = toolChoiceResult.extraSystemPrompt; } catch (err) { + logWarn(`openresponses: tool configuration failed: ${String(err)}`); sendJson(res, 400, { - error: { message: String(err), type: "invalid_request_error" }, + error: { message: "invalid tool configuration", type: "invalid_request_error" }, }); return true; } @@ -617,12 +620,13 @@ export async function handleOpenResponsesHttpRequest( sendJson(res, 200, response); } catch (err) { + logWarn(`openresponses: non-stream response failed: ${String(err)}`); const response = createResponseResource({ id: responseId, model, status: "failed", output: [], - error: { code: "api_error", message: String(err) }, + error: { code: "api_error", message: "internal error" }, }); sendJson(res, 500, response); } @@ -912,6 +916,7 @@ export async function handleOpenResponsesHttpRequest( }); } } catch (err) { + logWarn(`openresponses: streaming response failed: ${String(err)}`); if (closed) { return; } @@ -922,7 +927,7 @@ export async function handleOpenResponsesHttpRequest( model, status: "failed", output: [], - error: { code: "api_error", message: String(err) }, + error: { code: "api_error", message: "internal error" }, usage: finalUsage, }); diff --git a/src/gateway/tools-invoke-http.ts b/src/gateway/tools-invoke-http.ts index 813d122f787..24e91754cd2 100644 --- a/src/gateway/tools-invoke-http.ts +++ b/src/gateway/tools-invoke-http.ts @@ -348,9 +348,10 @@ export async function handleToolsInvokeHttpRequest( const result = await (tool as any).execute?.(`http-${Date.now()}`, toolArgs); sendJson(res, 200, { ok: true, result }); } catch (err) { + logWarn(`tools-invoke: tool execution failed: ${String(err)}`); sendJson(res, 400, { ok: false, - error: { type: "tool_error", message: err instanceof Error ? err.message : String(err) }, + error: { type: "tool_error", message: "tool execution failed" }, }); }