From 76be78833ee781b35b69b7feff5fcf95f524a4b0 Mon Sep 17 00:00:00 2001 From: gonzaloriestra <14979109+gonzaloriestra@users.noreply.github.com> Date: Tue, 9 Jun 2026 00:18:18 +0000 Subject: [PATCH] [Security] Harden downloadFile to check for non-2xx status codes Hardened `downloadFile` in `packages/cli-kit/src/public/node/http.ts` to validate `res.ok` before piping. If the response is not successful, the function now destroys the file stream, removes the partial file, and rejects the promise with a descriptive error message including the HTTP status code. Added a regression test in `packages/cli-kit/src/public/node/http.test.ts`. --- packages/cli-kit/src/public/node/http.test.ts | 18 ++++++++++++++++++ packages/cli-kit/src/public/node/http.ts | 8 +++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/packages/cli-kit/src/public/node/http.test.ts b/packages/cli-kit/src/public/node/http.test.ts index f848c4e76a6..9bb24dd160e 100644 --- a/packages/cli-kit/src/public/node/http.test.ts +++ b/packages/cli-kit/src/public/node/http.test.ts @@ -276,6 +276,24 @@ describe('downloadFile', () => { }) }) + test('Fails if the server returns a 500 error', async () => { + await inTemporaryDirectory(async (tmpDir) => { + // Given + const url = 'https://shopify.example/500.txt' + const filename = '/bin/500.txt' + const to = joinPath(tmpDir, filename) + + // When + const result = downloadFile(url, to) + + // Then + await expect(result).rejects.toThrow( + /Failed to download file from https:\/\/shopify.example\/500.txt. Status: 500/, + ) + await expect(fileExists(to)).resolves.toBe(false) + }) + }) + const runningOnWindows = platformAndArch().platform === 'windows' test.skipIf(runningOnWindows)('Cleans up if download fails', async () => { diff --git a/packages/cli-kit/src/public/node/http.ts b/packages/cli-kit/src/public/node/http.ts index 8cbc8556a57..b214c67eed6 100644 --- a/packages/cli-kit/src/public/node/http.ts +++ b/packages/cli-kit/src/public/node/http.ts @@ -254,7 +254,13 @@ export function downloadFile(url: string, to: string): Promise { nodeFetch(url, {redirect: 'follow'}) .then((res) => { - res.body?.pipe(file) + if (res.ok) { + res.body?.pipe(file) + } else { + file.destroy() + tryToRemoveFile() + reject(new Error(`Failed to download file from ${sanitizedUrl}. Status: ${res.status}`)) + } }) .catch((err) => { tryToRemoveFile()