From 9b853ce960f1bcbc33fde3a6b7f677152a6bf889 Mon Sep 17 00:00:00 2001 From: jeremymcs Date: Fri, 10 Apr 2026 15:06:01 -0400 Subject: [PATCH] fix(mcp-server): URL scheme regex no longer matches Windows drive letters Change /^[a-z]+:/i to /^[a-z]{2,}:/i in getWriteGateModuleCandidates() and getWorkflowExecutorModuleCandidates() so single-letter drive prefixes (C:, D:) are not rejected as URL schemes. All IANA-registered schemes are 2+ characters, so this is a safe narrowing. Adds regression tests for the regex fix. Fixes #3942 --- .../mcp-server/src/workflow-tools.test.ts | 28 +++++++++++++++++++ packages/mcp-server/src/workflow-tools.ts | 4 +-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/packages/mcp-server/src/workflow-tools.test.ts b/packages/mcp-server/src/workflow-tools.test.ts index 35a883b3b..74ee74a85 100644 --- a/packages/mcp-server/src/workflow-tools.test.ts +++ b/packages/mcp-server/src/workflow-tools.test.ts @@ -974,3 +974,31 @@ describe("workflow MCP tools", () => { } }); }); + +describe("URL scheme regex — Windows drive letter safety", () => { + // This is the regex used in getWriteGateModuleCandidates() and + // getWorkflowExecutorModuleCandidates() to reject non-file URL schemes. + // It must NOT match single-letter Windows drive prefixes (C:, D:, etc.). + const urlSchemeRegex = /^[a-z]{2,}:/i; + + it("rejects multi-letter URL schemes", () => { + assert.ok(urlSchemeRegex.test("http://example.com"), "http: should match"); + assert.ok(urlSchemeRegex.test("https://example.com"), "https: should match"); + assert.ok(urlSchemeRegex.test("ftp://files.example.com"), "ftp: should match"); + assert.ok(urlSchemeRegex.test("file:///C:/Users"), "file: should match"); + assert.ok(urlSchemeRegex.test("node:fs"), "node: should match"); + }); + + it("allows single-letter Windows drive prefixes", () => { + assert.ok(!urlSchemeRegex.test("C:\\Users\\user\\project"), "C:\\ should not match"); + assert.ok(!urlSchemeRegex.test("D:\\other\\path"), "D:\\ should not match"); + assert.ok(!urlSchemeRegex.test("c:\\lowercase\\drive"), "c:\\ should not match"); + assert.ok(!urlSchemeRegex.test("E:/forward/slash/path"), "E:/ should not match"); + }); + + it("allows bare filesystem paths", () => { + assert.ok(!urlSchemeRegex.test("/usr/local/lib/module.js"), "unix absolute path should not match"); + assert.ok(!urlSchemeRegex.test("./relative/path.js"), "relative path should not match"); + assert.ok(!urlSchemeRegex.test("../parent/path.js"), "parent relative path should not match"); + }); +}); diff --git a/packages/mcp-server/src/workflow-tools.ts b/packages/mcp-server/src/workflow-tools.ts index 95ea20494..1f3b3af97 100644 --- a/packages/mcp-server/src/workflow-tools.ts +++ b/packages/mcp-server/src/workflow-tools.ts @@ -318,7 +318,7 @@ function getWriteGateModuleCandidates(): string[] { const candidates: string[] = []; const explicitModule = process.env.GSD_WORKFLOW_WRITE_GATE_MODULE?.trim(); if (explicitModule) { - if (/^[a-z]+:/i.test(explicitModule) && !explicitModule.startsWith("file:")) { + if (/^[a-z]{2,}:/i.test(explicitModule) && !explicitModule.startsWith("file:")) { throw new Error("GSD_WORKFLOW_WRITE_GATE_MODULE only supports file: URLs or filesystem paths."); } candidates.push(explicitModule.startsWith("file:") ? explicitModule : toFileUrl(explicitModule)); @@ -340,7 +340,7 @@ function getWorkflowExecutorModuleCandidates(env: NodeJS.ProcessEnv = process.en const candidates: string[] = []; const explicitModule = env.GSD_WORKFLOW_EXECUTORS_MODULE?.trim(); if (explicitModule) { - if (/^[a-z]+:/i.test(explicitModule) && !explicitModule.startsWith("file:")) { + if (/^[a-z]{2,}:/i.test(explicitModule) && !explicitModule.startsWith("file:")) { throw new Error("GSD_WORKFLOW_EXECUTORS_MODULE only supports file: URLs or filesystem paths."); } candidates.push(explicitModule.startsWith("file:") ? explicitModule : toFileUrl(explicitModule));