From ad6a6a67b5658e58eacb2af8cac17ea836dd976c Mon Sep 17 00:00:00 2001 From: Alec McLeod Date: Mon, 8 Jun 2026 14:32:55 -0300 Subject: [PATCH] Escape regex metacharacters in variable key during substitution replaceVariables built its match pattern with `new RegExp(\`\\$\\{${key}\\}\`)`, interpolating the raw key. Keys contain "." (e.g. "user_config.cs_password") and the manifest schema allows arbitrary user_config key names, so the pattern over-matched ("." matched any character) and a key with a metacharacter (e.g. "[", "(a+)+") threw a SyntaxError / ReDoS, aborting config generation for an otherwise valid manifest. Escape the key before building the regex. Distinct from #258 (which concerns `$` in the replacement value). Adds tests for literal-dot matching and metacharacter-key safety. Fixes #262 --- src/shared/config.ts | 7 ++++++- test/config.test.ts | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/shared/config.ts b/src/shared/config.ts index 7073f58..e13ce51 100644 --- a/src/shared/config.ts +++ b/src/shared/config.ts @@ -24,7 +24,12 @@ export function replaceVariables( // Replace all variables in the string for (const [key, replacement] of Object.entries(variables)) { - const pattern = new RegExp(`\\$\\{${key}\\}`, "g"); + // Escape regex metacharacters in the key before building the pattern. + // Keys contain "." (e.g. "user_config.cs_password") and the manifest + // schema allows arbitrary user_config key names, so an unescaped key + // would over-match (any char for ".") or throw/ReDoS on a metacharacter. + const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const pattern = new RegExp(`\\$\\{${escapedKey}\\}`, "g"); // Check if this pattern actually exists in the string if (result.match(pattern)) { diff --git a/test/config.test.ts b/test/config.test.ts index 7e29f69..285a313 100644 --- a/test/config.test.ts +++ b/test/config.test.ts @@ -75,6 +75,26 @@ describe("replaceVariables", () => { const result = replaceVariables("Hello ${unknown}!", {}); expect(result).toBe("Hello ${unknown}!"); }); + + it("should treat '.' in the key as a literal, not a regex wildcard", () => { + // The "." must match only a literal dot. A template with a different + // character in that position must NOT be substituted. + const result = replaceVariables("${user_config_cs_password}", { + "user_config.cs_password": "SECRET", + }); + expect(result).toBe("${user_config_cs_password}"); + }); + + it("should not throw on keys containing regex metacharacters", () => { + // A user_config key with a metacharacter (e.g. "[") must not crash regex + // construction; it should match its own literal placeholder. + expect(() => + replaceVariables("x ${a[b} y", { "a[b": "VALUE" }), + ).not.toThrow(); + expect(replaceVariables("x ${a[b} y", { "a[b": "VALUE" })).toBe( + "x VALUE y", + ); + }); }); describe("getMcpConfigForManifest", () => {