From b0da6a3bbee8f63fafb72c1e552bbdc8d52d7dfc Mon Sep 17 00:00:00 2001 From: gonzaloriestra <14979109+gonzaloriestra@users.noreply.github.com> Date: Wed, 8 Jul 2026 00:30:15 +0000 Subject: [PATCH] [Security] Harden notifications URL validation This change ensures that the URL used for fetching CLI notifications, whether default or provided via the `SHOPIFY_CLI_NOTIFICATIONS_URL` environment variable, is a valid HTTPS URL. This prevents potential loading of malicious content from insecure or invalid sources. - Validates `SHOPIFY_CLI_NOTIFICATIONS_URL` using `globalThis.URL`. - Enforces `https:` protocol for notifications. - Falls back to default Shopify CDN URL if the provided URL is invalid or insecure. - Adds unit tests to verify the hardening. --- .../public/node/notifications-system.test.ts | 42 +++++++++++++++++++ .../src/public/node/notifications-system.ts | 20 ++++++++- 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/packages/cli-kit/src/public/node/notifications-system.test.ts b/packages/cli-kit/src/public/node/notifications-system.test.ts index 1eefbe57a37..52e68616d8b 100644 --- a/packages/cli-kit/src/public/node/notifications-system.test.ts +++ b/packages/cli-kit/src/public/node/notifications-system.test.ts @@ -1,10 +1,12 @@ import { Notification, + fetchNotifications, fetchNotificationsInBackground, filterNotifications, showNotificationsIfNeeded, } from './notifications-system.js' import {renderError, renderInfo, renderWarning} from './ui.js' +import {fetch} from './http.js' import {sniffForJson} from './path.js' import {exec} from './system.js' import {cacheRetrieve} from '../../private/node/conf-store.js' @@ -14,6 +16,7 @@ vi.mock('./ui.js') vi.mock('../../private/node/conf-store.js') vi.mock('./path.js') vi.mock('./system.js') +vi.mock('./http.js') const betweenVersins1and2: Notification = { id: 'betweenVersins1and2', @@ -456,3 +459,42 @@ describe('fetchNotificationsInBackground', () => { ) }) }) + +describe('fetchNotifications', () => { + test('uses the default URL when SHOPIFY_CLI_NOTIFICATIONS_URL is not secure', async () => { + // Given + vi.stubEnv('SHOPIFY_CLI_NOTIFICATIONS_URL', 'http://malicious.com/notifications.json') + vi.mocked(fetch).mockResolvedValue({ + status: 200, + text: () => Promise.resolve(JSON.stringify({notifications: []})), + } as any) + + // When + await fetchNotifications() + + // Then + expect(fetch).toHaveBeenCalledWith( + 'https://cdn.shopify.com/static/cli/notifications.json', + undefined, + expect.anything(), + ) + vi.unstubAllEnvs() + }) + + test('uses the provided URL when SHOPIFY_CLI_NOTIFICATIONS_URL is secure', async () => { + // Given + const secureUrl = 'https://my-secure-repo.com/notifications.json' + vi.stubEnv('SHOPIFY_CLI_NOTIFICATIONS_URL', secureUrl) + vi.mocked(fetch).mockResolvedValue({ + status: 200, + text: () => Promise.resolve(JSON.stringify({notifications: []})), + } as any) + + // When + await fetchNotifications() + + // Then + expect(fetch).toHaveBeenCalledWith(secureUrl, undefined, expect.anything()) + vi.unstubAllEnvs() + }) +}) diff --git a/packages/cli-kit/src/public/node/notifications-system.ts b/packages/cli-kit/src/public/node/notifications-system.ts index d9eade1f1cb..04dcfeb738e 100644 --- a/packages/cli-kit/src/public/node/notifications-system.ts +++ b/packages/cli-kit/src/public/node/notifications-system.ts @@ -24,7 +24,25 @@ const COMMANDS_TO_SKIP = [ ] function url(): string { - return process.env.SHOPIFY_CLI_NOTIFICATIONS_URL ?? URL + const envUrl = process.env.SHOPIFY_CLI_NOTIFICATIONS_URL + if (envUrl) { + try { + const parsedUrl = new globalThis.URL(envUrl) + // We only allow https for notifications to prevent loading malicious content from insecure sources. + if (parsedUrl.protocol === 'https:') { + return envUrl + } + outputDebug( + `The notifications URL provided via SHOPIFY_CLI_NOTIFICATIONS_URL (${envUrl}) is not secure (https). Falling back to default.`, + ) + // eslint-disable-next-line no-catch-all/no-catch-all + } catch { + outputDebug( + `The notifications URL provided via SHOPIFY_CLI_NOTIFICATIONS_URL (${envUrl}) is not a valid URL. Falling back to default.`, + ) + } + } + return URL } const NotificationSchema = zod.object({