From 39e1ce55bccb4ab1761c1edee7a5bfbefe6c1a49 Mon Sep 17 00:00:00 2001 From: Niels Kootstra <545768+nkootstra@users.noreply.github.com> Date: Mon, 28 Sep 2026 06:44:35 +0200 Subject: [PATCH] fix(core): reject relative path segments in repository hosts (#51577) --- packages/core/src/repository.ts | 3 ++- packages/core/test/repository.test.ts | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/core/src/repository.ts b/packages/core/src/repository.ts index 3f14477d45b..5233e50b440 100644 --- a/packages/core/src/repository.ts +++ b/packages/core/src/repository.ts @@ -145,8 +145,9 @@ function parts(input: string) { .filter(Boolean) } +// cachePath makes each `:`-separated host part a directory. function safeHost(input: string) { - return Boolean(input) && !input.startsWith("-") && !/[\s/\\]/.test(input) + return Boolean(input) && !input.startsWith("-") && input.split(":").every(safeSegment) } function safeSegment(input: string) { diff --git a/packages/core/test/repository.test.ts b/packages/core/test/repository.test.ts index a877b6dd92b..ab41ce7fddb 100644 --- a/packages/core/test/repository.test.ts +++ b/packages/core/test/repository.test.ts @@ -41,6 +41,12 @@ describe("Repository", () => { }) }) + test("caches a host with a port under a directory per host part", () => { + expect(Repository.cachePath("/cache", Repository.parseRemote("ssh://git@example.com:2222/owner/repo"))).toBe( + path.join("/cache", "example.com", "2222", "owner", "repo"), + ) + }) + test("keeps local file repositories distinct from remote repositories", () => { const localPath = path.resolve("repo.git") const reference = Repository.parse(pathToFileURL(localPath).href) @@ -62,6 +68,18 @@ describe("Repository", () => { expect(() => Repository.validateBranch("bad branch")).toThrow(Repository.InvalidBranchError) }) + test.each([ + "..:repo", + "git@..:repo", + "../owner/repo", + "ssh://../repo", + "ssh://..:22/repo", + "https://%2e%2e/owner/repo", + ".:repo", + ])("rejects %s because its host contains a relative path segment", (input) => { + expect(() => Repository.parseRemote(input)).toThrow(Repository.InvalidReferenceError) + }) + test("compares cache identity independent of input spelling", () => { const shorthand = Repository.parseRemote("owner/repo")