From c95f80dd75ede9a72b9fbfd7dc600604ef6cb2e4 Mon Sep 17 00:00:00 2001 From: Mert Koseoglu Date: Thu, 19 Mar 2026 21:40:18 +0300 Subject: [PATCH] fix: ReDoS prevention + bound slugify fallback scan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Validate elicitation keys match variable name format before regex construction (prevents ReDoS via user-controlled regex metacharacters — CWE-1333) - Narrow slugify fallback to only prompts with slug=null, cap at 500 results - Add TODO to backfill slug column so fallback can be removed Co-Authored-By: Claude Opus 4.6 (1M context) --- src/pages/api/mcp.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/pages/api/mcp.ts b/src/pages/api/mcp.ts index de749d1d..77e0d3b7 100644 --- a/src/pages/api/mcp.ts +++ b/src/pages/api/mcp.ts @@ -220,12 +220,14 @@ function createServer(options: ServerOptions = {}) { }); } // Fallback: lookup by slugified title (for prompts without stored slug) + // TODO: Backfill slug column for all existing prompts so this fallback can be removed if (!prompt) { - const allPrompts = await db.prompt.findMany({ - where: promptFilter, + const unslugged = await db.prompt.findMany({ + where: { ...promptFilter, slug: null }, select: promptSelect, + take: 500, }); - prompt = allPrompts.find((p) => slugify(p.title) === promptSlug) || null; + prompt = unslugged.find((p) => slugify(p.title) === promptSlug) || null; } if (!prompt) { @@ -453,6 +455,10 @@ function createServer(options: ServerOptions = {}) { if (elicitResult.action === "accept" && elicitResult.content) { let filledContent = prompt.content; for (const [key, value] of Object.entries(elicitResult.content)) { + // Skip keys that don't match valid variable name format (ReDoS prevention) + if (!/^[a-zA-Z_][a-zA-Z0-9_\s]*$/.test(key)) { + continue; + } // Replace ${key} or ${key:default} patterns filledContent = filledContent.replace( new RegExp(`\\$\\{${key}(?::[^}]*)?\\}`, "g"),