From e0fc1c72620ab2f3db4b4be6defdab3dfb8f5653 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Sat, 11 Apr 2026 17:38:15 -0400 Subject: [PATCH] fix(server): sanitize error responses to prevent stack trace exposure Replace raw error objects passed to res.json() with generic sanitized messages so internal error details are not leaked to clients. Full errors continue to be logged server-side via console.error. Also adds a missing return in the /sse ECONNREFUSED branch, which previously fell through and attempted a second response after headers had already been sent. Resolves CodeQL js/stack-trace-exposure alerts in server/src/index.ts. Co-Authored-By: Claude Opus 4.6 (1M context) --- server/src/index.ts | 54 +++++++++++++++++++++++++++++++-------------- 1 file changed, 38 insertions(+), 16 deletions(-) diff --git a/server/src/index.ts b/server/src/index.ts index e6af55e5f..5b08571b4 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -72,13 +72,26 @@ const is401Error = (error: unknown): boolean => { return false; }; +/** + * Sends a sanitized JSON error response to the client without exposing + * stack traces or internal error details. The full error is expected to + * have already been logged server-side via console.error. + */ +const sendErrorResponse = ( + res: express.Response, + status: number, + message: string, +) => { + res.status(status).json({ error: message }); +}; + /** * Prefer forwarding the upstream MCP 401 (WWW-Authenticate + body) so the browser - * matches direct-mode OAuth behavior. Falls back to JSON-encoding `error` if unknown. + * matches direct-mode OAuth behavior. Falls back to a generic 401 JSON response + * when no upstream details were captured, to avoid leaking stack traces. */ const sendProxiedUnauthorized = ( res: express.Response, - error: unknown, headerHolder?: ProxyHeaderHolder, ) => { const captured = headerHolder?.lastUpstream401; @@ -92,7 +105,7 @@ const sendProxiedUnauthorized = ( delete headerHolder.lastUpstream401; return; } - res.status(401).json(error); + sendErrorResponse(res, 401, "Unauthorized"); }; // Function to get HTTP headers. @@ -509,7 +522,7 @@ app.get( } } catch (error) { console.error("Error in /mcp route:", error); - res.status(500).json(error); + sendErrorResponse(res, 500, "Internal server error"); } }, ); @@ -545,7 +558,7 @@ app.post( } } catch (error) { console.error("Error in /mcp route:", error); - res.status(500).json(error); + sendErrorResponse(res, 500, "Internal server error"); } } else { console.log("New StreamableHttp connection request"); @@ -592,11 +605,11 @@ app.post( "Received 401 Unauthorized from MCP server:", error instanceof Error ? error.message : error, ); - sendProxiedUnauthorized(res, error, streamableHeaderHolder); + sendProxiedUnauthorized(res, streamableHeaderHolder); return; } console.error("Error in /mcp POST route:", error); - res.status(500).json(error); + sendErrorResponse(res, 500, "Internal server error"); } } }, @@ -627,7 +640,7 @@ app.delete( res.status(200).end(); } catch (error) { console.error("Error in /mcp route:", error); - res.status(500).json(error); + sendErrorResponse(res, 500, "Internal server error"); } } }, @@ -732,11 +745,11 @@ app.get( console.error( "Received 401 Unauthorized from MCP server. Authentication failure.", ); - sendProxiedUnauthorized(res, error, undefined); + sendProxiedUnauthorized(res, undefined); return; } console.error("Error in /stdio route:", error); - res.status(500).json(error); + sendErrorResponse(res, 500, "Internal server error"); } }, ); @@ -781,20 +794,29 @@ app.get( console.error( "Received 401 Unauthorized from MCP server. Authentication failure.", ); - sendProxiedUnauthorized(res, error, sseHeaderHolder); + sendProxiedUnauthorized(res, sseHeaderHolder); return; } else if (error instanceof SseError && error.code === 404) { console.error( "Received 404 not found from MCP server. Does the MCP server support SSE?", ); - res.status(404).json(error); + sendErrorResponse( + res, + 404, + "MCP server returned 404. Does it support SSE?", + ); return; } else if (JSON.stringify(error).includes("ECONNREFUSED")) { console.error("Connection refused. Is the MCP server running?"); - res.status(500).json(error); + sendErrorResponse( + res, + 500, + "Connection refused. Is the MCP server running?", + ); + return; } console.error("Error in /sse route:", error); - res.status(500).json(error); + sendErrorResponse(res, 500, "Internal server error"); } }, ); @@ -824,7 +846,7 @@ app.post( await transport.handlePostMessage(req, res); } catch (error) { console.error("Error in /message route:", error); - res.status(500).json(error); + sendErrorResponse(res, 500, "Internal server error"); } }, ); @@ -900,7 +922,7 @@ app.get("/config", originValidationMiddleware, authMiddleware, (req, res) => { }); } catch (error) { console.error("Error in /config route:", error); - res.status(500).json(error); + sendErrorResponse(res, 500, "Internal server error"); } });