diff --git a/packages/app/src/cli/api/graphql/business-platform-organizations/generated/types.d.ts b/packages/app/src/cli/api/graphql/business-platform-organizations/generated/types.d.ts index 52e20a2dd4..fd627c21b2 100644 --- a/packages/app/src/cli/api/graphql/business-platform-organizations/generated/types.d.ts +++ b/packages/app/src/cli/api/graphql/business-platform-organizations/generated/types.d.ts @@ -22,6 +22,8 @@ export type Scalars = { ActionAuditID: { input: any; output: any; } /** The ID for a Address. */ AddressID: { input: any; output: any; } + /** The ID for a Attestation. */ + AttestationID: { input: any; output: any; } /** The ID for a BulkDataOperation. */ BulkDataOperationID: { input: any; output: any; } /** The ID for a BusinessUser. */ diff --git a/packages/cli-kit/src/public/node/system.test.ts b/packages/cli-kit/src/public/node/system.test.ts index ed12516ba4..9a771c3619 100644 --- a/packages/cli-kit/src/public/node/system.test.ts +++ b/packages/cli-kit/src/public/node/system.test.ts @@ -2,10 +2,12 @@ import * as system from './system.js' import {execa} from 'execa' import {describe, expect, test, vi} from 'vitest' import which from 'which' +import open from 'open' import {Readable} from 'stream' import * as fs from 'fs' +vi.mock('open') vi.mock('which') vi.mock('execa') vi.mock('fs', async (importOriginal) => { @@ -16,6 +18,80 @@ vi.mock('fs', async (importOriginal) => { } }) +describe('openURL', () => { + test('opens http URLs', async () => { + // Given + const url = 'http://shopify.com' + + // When + const got = await system.openURL(url) + + // Then + expect(got).toBe(true) + expect(open).toHaveBeenCalledWith(url) + }) + + test('opens https URLs', async () => { + // Given + const url = 'https://shopify.com' + + // When + const got = await system.openURL(url) + + // Then + expect(got).toBe(true) + expect(open).toHaveBeenCalledWith(url) + }) + + test('opens file URLs', async () => { + // Given + const url = 'file:///path/to/file.html' + + // When + const got = await system.openURL(url) + + // Then + expect(got).toBe(true) + expect(open).toHaveBeenCalledWith(url) + }) + + test('blocks javascript URLs', async () => { + // Given + const url = 'javascript:alert("hello")' + + // When + const got = await system.openURL(url) + + // Then + expect(got).toBe(false) + expect(open).not.toHaveBeenCalled() + }) + + test('blocks data URLs', async () => { + // Given + const url = 'data:text/html,hi' + + // When + const got = await system.openURL(url) + + // Then + expect(got).toBe(false) + expect(open).not.toHaveBeenCalled() + }) + + test('returns false for invalid URLs', async () => { + // Given + const url = 'not-a-url' + + // When + const got = await system.openURL(url) + + // Then + expect(got).toBe(false) + expect(open).not.toHaveBeenCalled() + }) +}) + describe('captureOutput', () => { test('runs the command when it is not found in the current directory', async () => { // Given diff --git a/packages/cli-kit/src/public/node/system.ts b/packages/cli-kit/src/public/node/system.ts index e2d5cbeead..96b672e96f 100644 --- a/packages/cli-kit/src/public/node/system.ts +++ b/packages/cli-kit/src/public/node/system.ts @@ -53,6 +53,16 @@ interface BuildExecOptions { export async function openURL(url: string): Promise { const externalOpen = await import('open') try { + const parsed = new URL(url) + const allowedProtocols = ['http:', 'https:', 'file:'] + + // Security: Validate protocol to prevent execution of dangerous protocols + // (e.g. javascript:, data:, etc.) via the system opener. + if (!allowedProtocols.includes(parsed.protocol)) { + outputDebug(`Skipped opening URL with unsecure protocol: ${parsed.protocol}`) + return false + } + await externalOpen.default(url) return true // eslint-disable-next-line no-catch-all/no-catch-all