From 4ca27824ac0115299ff990effa4f48b823fb7a51 Mon Sep 17 00:00:00 2001 From: Max Golovanov Date: Fri, 1 May 2026 15:06:01 -0700 Subject: [PATCH 1/3] fix(http): prevent recursive session cleanup stack overflow --- src/http-server.ts | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/src/http-server.ts b/src/http-server.ts index 5db4c9a..85d7466 100644 --- a/src/http-server.ts +++ b/src/http-server.ts @@ -51,6 +51,7 @@ const refreshIntervalMs = parseInt(process.env.ADVISORY_REFRESH_INTERVAL_MS || ' // Store transports by session ID const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {}; const sessionTimeouts: Record = {}; +const cleanupInProgress = new Set(); const SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes // Periodic refresh cleanup function @@ -60,19 +61,30 @@ let stopPeriodicRefresh: (() => void) | null = null; * Clean up expired session */ function cleanupSession(sessionId: string): void { + if (cleanupInProgress.has(sessionId)) { + return; + } + + cleanupInProgress.add(sessionId); logger.info('Cleaning up session', { sessionId }); - const transport = transports[sessionId]; - if (transport) { - try { - transport.close(); - } catch (error) { - logger.warn('Error closing transport', { sessionId, error: error instanceof Error ? error.message : String(error) }); + try { + const transport = transports[sessionId]; + if (transport) { + // Remove from registry first and detach onclose to avoid recursive cleanup. + delete transports[sessionId]; + transport.onclose = undefined; + try { + transport.close(); + } catch (error) { + logger.warn('Error closing transport', { sessionId, error: error instanceof Error ? error.message : String(error) }); + } } - delete transports[sessionId]; - } - if (sessionTimeouts[sessionId]) { - clearTimeout(sessionTimeouts[sessionId]); - delete sessionTimeouts[sessionId]; + if (sessionTimeouts[sessionId]) { + clearTimeout(sessionTimeouts[sessionId]); + delete sessionTimeouts[sessionId]; + } + } finally { + cleanupInProgress.delete(sessionId); } } From 8354b98969d6c3ea1b4f5259657f148d50f53eba Mon Sep 17 00:00:00 2001 From: Max Golovanov Date: Fri, 1 May 2026 15:30:52 -0700 Subject: [PATCH 2/3] Potential fix for pull request finding 'CodeQL / Prototype-polluting assignment' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- src/http-server.ts | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/http-server.ts b/src/http-server.ts index 85d7466..f03186d 100644 --- a/src/http-server.ts +++ b/src/http-server.ts @@ -68,10 +68,10 @@ function cleanupSession(sessionId: string): void { cleanupInProgress.add(sessionId); logger.info('Cleaning up session', { sessionId }); try { - const transport = transports[sessionId]; + const transport = transports.get(sessionId); if (transport) { // Remove from registry first and detach onclose to avoid recursive cleanup. - delete transports[sessionId]; + transports.delete(sessionId); transport.onclose = undefined; try { transport.close(); @@ -79,9 +79,10 @@ function cleanupSession(sessionId: string): void { logger.warn('Error closing transport', { sessionId, error: error instanceof Error ? error.message : String(error) }); } } - if (sessionTimeouts[sessionId]) { - clearTimeout(sessionTimeouts[sessionId]); - delete sessionTimeouts[sessionId]; + const timeout = sessionTimeouts.get(sessionId); + if (timeout) { + clearTimeout(timeout); + sessionTimeouts.delete(sessionId); } } finally { cleanupInProgress.delete(sessionId); @@ -92,13 +93,14 @@ function cleanupSession(sessionId: string): void { * Reset session timeout */ function resetSessionTimeout(sessionId: string): void { - if (sessionTimeouts[sessionId]) { - clearTimeout(sessionTimeouts[sessionId]); + const existingTimeout = sessionTimeouts.get(sessionId); + if (existingTimeout) { + clearTimeout(existingTimeout); } - sessionTimeouts[sessionId] = setTimeout(() => { + sessionTimeouts.set(sessionId, setTimeout(() => { logger.info('Session timeout', { sessionId }); cleanupSession(sessionId); - }, SESSION_TIMEOUT_MS); + }, SESSION_TIMEOUT_MS)); } // Store local API server instance @@ -166,9 +168,9 @@ app.post("/mcp", async (req: Request, res: Response) => { try { let transport: StreamableHTTPServerTransport; - if (sessionId && transports[sessionId]) { + if (sessionId && transports.has(sessionId)) { // Reuse existing transport and reset timeout - transport = transports[sessionId]; + transport = transports.get(sessionId)!; resetSessionTimeout(sessionId); await transport.handleRequest(req, res, req.body); } else if (!sessionId && isInitializeRequest(req.body)) { @@ -184,7 +186,7 @@ app.post("/mcp", async (req: Request, res: Response) => { }); // Store transport - transports[newSessionId] = transport; + transports.set(newSessionId, transport); // Set up session timeout resetSessionTimeout(newSessionId); From a8b892f45c886e54d2c86a01502dce3a158ad306 Mon Sep 17 00:00:00 2001 From: Max Golovanov Date: Fri, 1 May 2026 15:35:18 -0700 Subject: [PATCH 3/3] fix: change transports and sessionTimeouts to Map to fix TypeScript errors after CodeQL suggestion --- src/http-server.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/http-server.ts b/src/http-server.ts index f03186d..b537877 100644 --- a/src/http-server.ts +++ b/src/http-server.ts @@ -49,8 +49,8 @@ const refreshOnStart = process.env.ADVISORY_REFRESH_ON_START !== 'false'; // Def const refreshIntervalMs = parseInt(process.env.ADVISORY_REFRESH_INTERVAL_MS || '0'); // Default: disabled (0) // Store transports by session ID -const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {}; -const sessionTimeouts: Record = {}; +const transports = new Map(); +const sessionTimeouts = new Map(); const cleanupInProgress = new Set(); const SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes