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
This commit is contained in:
jeremymcs 2026-04-10 15:06:01 -04:00
parent 9b7f151964
commit 9b853ce960
2 changed files with 30 additions and 2 deletions

View file

@ -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");
});
});

View file

@ -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));