From 88b768403495e87fd2bbe60349040d7a927734b2 Mon Sep 17 00:00:00 2001 From: joaovictor91123 Date: Thu, 16 Jul 2026 08:37:58 +0400 Subject: [PATCH] fix(mcp): redact a parenthesis/bracket/colon-prefixed local path in redactLocalPath redactLocalPath's prefix delimiter class only recognized whitespace, quotes, backtick, and "=" immediately before a local path -- missing "(", the Node.js stack-frame shape ("at fn (/abs/path:10:5)"), so a real absolute path pasted into validation output could leak upstream unredacted. Verified and closed two adjacent gaps in the same class ("[" and ":") while at it, and excluded "]" from the path-tail/segment patterns so a bracket-wrapped path redacts cleanly instead of swallowing the closing bracket. --- packages/loopover-mcp/lib/redact-local-path.js | 9 ++++++--- test/unit/redact-local-path.test.ts | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/packages/loopover-mcp/lib/redact-local-path.js b/packages/loopover-mcp/lib/redact-local-path.js index ddd5745418..00379bd8a4 100644 --- a/packages/loopover-mcp/lib/redact-local-path.js +++ b/packages/loopover-mcp/lib/redact-local-path.js @@ -23,9 +23,12 @@ export function redactLocalPath(value) { // Both `/g` patterns are rebuilt per call so no `lastIndex` state carries between invocations. // Delimiter-anchored roots (`~/`, `~\`, `C:\`, `C:/`, `/`) whose interior segments may contain // spaces, e.g. `/Users/Alice Smith/project` — the anchoring prefix is preserved, only the path swaps. - const pathSegment = "[^\\\\/\\s\"'`,;)]+(?:\\s+[^\\\\/\\s\"'`,;)]+)*(?=[\\\\/])"; - const pathTail = "[^\\\\/\\s\"'`,;)]+"; - const rootedPath = new RegExp(`(^|[\\s"'\\\`=])((?:~[\\\\/]|[A-Za-z]:[\\\\/]|/)(?:${pathSegment}[\\\\/])*${pathTail})`, "g"); + const pathSegment = "[^\\\\/\\s\"'`,;)\\]]+(?:\\s+[^\\\\/\\s\"'`,;)\\]]+)*(?=[\\\\/])"; + const pathTail = "[^\\\\/\\s\"'`,;)\\]]+"; + // Prefix delimiters a real path can immediately follow in pasted stack-trace/validation-output text. + // `(` is the Node.js stack-frame shape (`at fn (/abs/path:10:5)`); `[` and `:` cover the same "no space + // before the path" shape in bracketed log lines and colon-joined messages (e.g. `path:/abs/path`). + const rootedPath = new RegExp(`(^|[\\s"'\\\`=(\\[:])((?:~[\\\\/]|[A-Za-z]:[\\\\/]|/)(?:${pathSegment}[\\\\/])*${pathTail})`, "g"); return text .replace(rootedPath, (_, prefix) => `${prefix}`) // Home/Windows roots that appear mid-token with no leading delimiter (so the anchored pass skips diff --git a/test/unit/redact-local-path.test.ts b/test/unit/redact-local-path.test.ts index 1e988ea202..6b9ef3601c 100644 --- a/test/unit/redact-local-path.test.ts +++ b/test/unit/redact-local-path.test.ts @@ -30,6 +30,24 @@ describe("redactLocalPath (heuristic: detect an unknown path in free text)", () expect(redacted).not.toContain("~/private"); }); + // #6258: a Node.js stack-frame path (`at fn (/abs/path:line:col)`) previously leaked in full because + // `(` was missing from the prefix delimiter class -- only a space-prefixed path redacted correctly. + it("redacts a parenthesis-prefixed path (the Node.js stack-frame shape)", () => { + // The trailing `:10:5` line/column suffix has no path separator before the closing `)`, so it's + // swallowed into the same redacted match -- still a full, leak-free redaction either way. + expect(redactLocalPath("at Object. (/Users/alice/secretproject/file.js:10:5)")).toBe( + "at Object. ()", + ); + }); + + it("redacts a bracket-prefixed path, preserving the closing bracket", () => { + expect(redactLocalPath("[/Users/alice/secretproject/file.js]")).toBe("[]"); + }); + + it("redacts a colon-prefixed path with no space (e.g. `label:/abs/path`)", () => { + expect(redactLocalPath("error:/Users/alice/secretproject/file.js")).toBe("error:"); + }); + it("leaves text with no local path untouched, including a bare slash", () => { expect(redactLocalPath("just some text, version 1.2.3")).toBe("just some text, version 1.2.3"); expect(redactLocalPath("pass --flag / or | here")).toBe("pass --flag / or | here");