fix(setup): validate Hopper download channel URLs

This commit is contained in:
morluto committed 2026-10-27 09:26:40 +08:00
1 parent 25b69bb803
commit fef83241b1
3 files changed
+58 -5

No files matched your search

+32
View File
@@ -121,3 +121,35 @@ it("distinguishes malformed UTF-8 from a genuine replacement character and toler
parseHopperReleaseJson(new TextEncoder().encode(bom + '{"x":"雪�"}')),
).toMatchObject({ ok: true, value: { x: "雪�" } });
});
describe("macOS Hopper public download channel", () => {
const macRelease = {
filename: "https://www.hopperapp.com:443/downloader/public/Hopper-demo.dmg",
file_length: "123",
file_hash: "a".repeat(40),
};
it.each([
"https://www.hopperapp.com/downloader/public/Hopper-demo.dmg",
"https://WWW.HOPPERAPP.COM:443/downloader/public/Hopper-demo.dmg",
"https://www.hopperapp.com:443/downloader/public/Hopper%20demo.dmg",
])("preserves the admitted source spelling %s", (filename) => {
expect(
parseMacHopperReleases({ "OS X": { ...macRelease, filename } }),
).toEqual({ ok: true, value: { ...macRelease, filename } });
});
it.each([
"https://www.hopperapp.com:443/downloader/public/../private/Hopper-demo.dmg",
"https://www.hopperapp.com:443/downloader/public/%2e%2e/private/Hopper-demo.dmg",
"https://www.hopperapp.com:443/downloader/public/Hopper-demo.dmg?",
"https://www.hopperapp.com:443/downloader/public/Hopper-demo.dmg#",
"https://user:[email protected]:443/downloader/public/Hopper-demo.dmg",
"https://other.example/downloader/public/Hopper-demo.dmg",
"http://www.hopperapp.com/downloader/public/Hopper-demo.dmg",
])("rejects a URL outside the public channel: %s", (filename) => {
expect(
parseMacHopperReleases({ "OS X": { ...macRelease, filename } }),
).toMatchObject({ ok: false, error: { reason: "release_metadata" } });
});
});
+10 -5
View File
@@ -52,6 +52,14 @@ const selectedPlatformsSchema = z.object({
Arch: z.unknown().optional(),
});
/** Interpret channel identity once using the same URL semantics as the downloader. */
const isPublicHopperDownload = (url: URL): boolean =>
url.origin === "https://www.hopperapp.com" &&
url.username === "" &&
url.password === "" &&
!urlDeclaresQueryOrFragment(url) &&
url.pathname.startsWith("/downloader/public/");
/** Admit the selected macOS row without projecting away vendor release metadata. */
export const parseMacHopperReleases = (
raw: unknown,
@@ -68,7 +76,7 @@ export const parseMacHopperReleases = (
),
);
const retained = release.data as MacHopperRelease;
if (!retained.filename.startsWith(HOPPER_DOWNLOAD_PREFIX))
if (!isPublicHopperDownload(new URL(retained.filename)))
return err(
new HopperReleaseError(
"release_metadata",
@@ -154,10 +162,7 @@ export const parseLinuxHopperRelease = (
);
const url = new URL(parsed.data.filename);
if (
url.origin !== "https://www.hopperapp.com" ||
url.username !== "" ||
url.password !== "" ||
urlDeclaresQueryOrFragment(url) ||
!isPublicHopperDownload(url) ||
!url.pathname.startsWith("/downloader/public/Hopper-") ||
!url.pathname.endsWith(`-Linux-demo${suffixes[family]}`)
)
+16
View File
@@ -307,3 +307,19 @@ it.each([false, true])(
expect(host.installs).toBe(1);
},
);
it("refuses release URLs that normalize outside the public channel before package download", async () => {
const host = new FakeMacHopperHost();
const feed = await host.download("files-api");
const row = JSON.parse(new TextDecoder().decode(feed.bytes));
row["OS X"].filename =
"https://www.hopperapp.com:443/downloader/public/%2e%2e/private/Hopper-demo.dmg";
host.feedBytes = new TextEncoder().encode(JSON.stringify(row));
expect(await installMacHopper({}, host)).toMatchObject({
status: "failed",
reason: "release_metadata",
});
expect(host.packageDownloads).toBe(0);
expect(host.mounts).toBe(0);
});