From 8514fdf713d107197744beac765b9d891062283c Mon Sep 17 00:00:00 2001 From: ghost <49853598+JSONbored@users.noreply.github.com> Date: Tue, 9 Jun 2026 11:12:38 -0600 Subject: [PATCH] fix(raycast): reject unsafe base refs --- src/raycast/local-repo-analyzer.ts | 10 +++++- test/unit/raycast-local-repo-analyzer.test.ts | 31 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/raycast/local-repo-analyzer.ts b/src/raycast/local-repo-analyzer.ts index c9a6e45d7e..a74790731a 100644 --- a/src/raycast/local-repo-analyzer.ts +++ b/src/raycast/local-repo-analyzer.ts @@ -76,7 +76,8 @@ export function collectRaycastLocalRepoMetadata(input: { throw new Error("Raycast branch analysis supports metadata-only mode; source upload mode is rejected."); } const git = input.git ?? gitLines; - const baseRef = input.baseRef ?? git(input.cwd, ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"])[0]?.replace(/^origin\//, "") ?? "main"; + const rawBaseRef = input.baseRef ?? git(input.cwd, ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"])[0]?.replace(/^origin\//, "") ?? "main"; + const baseRef = validateSafeBaseRef(rawBaseRef); const remoteUrl = git(input.cwd, ["config", "--get", "remote.origin.url"])[0] ?? ""; const repoFullName = input.repoFullName ?? parseGitHubRemote(remoteUrl); if (!repoFullName) throw new Error("Could not infer repoFullName from the git remote; pass repoFullName explicitly."); @@ -200,6 +201,13 @@ async function postJson( return payload; } +function validateSafeBaseRef(baseRef: string): string { + if (!baseRef || baseRef.startsWith("-")) { + throw new Error("Unsafe git baseRef; pass a branch or ref name that does not begin with '-'."); + } + return baseRef; +} + function collectChangedFiles(cwd: string, baseRef: string, git: RaycastGitRunner): RaycastChangedFileMetadata[] { const numstat = new Map(parseNumstat(cwd, baseRef, git).map((entry) => [entry.path, entry])); return git(cwd, ["diff", "--name-status", "-M", baseRef, "--"]).map((row) => { diff --git a/test/unit/raycast-local-repo-analyzer.test.ts b/test/unit/raycast-local-repo-analyzer.test.ts index d65ab78e1f..d3fac0df7b 100644 --- a/test/unit/raycast-local-repo-analyzer.test.ts +++ b/test/unit/raycast-local-repo-analyzer.test.ts @@ -111,6 +111,37 @@ describe("Raycast local repo analyzer", () => { expect(calls.map((call) => call.split(" ")[0]).join("\n")).not.toMatch(/^(cat|show|grep|archive)$/m); }); + it("rejects inferred base refs that could be parsed as git options", () => { + const { git, calls } = fakeGit({ + "symbolic-ref --short refs/remotes/origin/HEAD": "origin/--output=/tmp/gittensory-owned\n", + }); + + expect(() => + collectRaycastLocalRepoMetadata({ + cwd: "/tmp/private-checkout", + login: "jsonbored", + repoFullName: "JSONbored/gittensory", + git, + }), + ).toThrow(/unsafe git baseRef/i); + expect(calls).toEqual(["symbolic-ref --short refs/remotes/origin/HEAD"]); + }); + + it("rejects explicit base refs that could be parsed as git options", () => { + const git = vi.fn(); + + expect(() => + collectRaycastLocalRepoMetadata({ + cwd: "/tmp/private-checkout", + login: "jsonbored", + repoFullName: "JSONbored/gittensory", + baseRef: "--output=/tmp/gittensory-owned", + git, + }), + ).toThrow(/unsafe git baseRef/i); + expect(git).not.toHaveBeenCalled(); + }); + it("rejects source upload mode before running git", () => { const git = vi.fn();