Merge pull request #3943 from Git-Scram/fix/mcp-windows-drive-letter-regex

fix(mcp-server): URL scheme regex matches Windows drive letters
This commit is contained in:
Jeremy McSpadden 2026-04-10 15:17:59 -05:00 committed by GitHub
commit c0f101a217
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));