Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
76 changes: 76 additions & 0 deletions packages/cli-kit/src/public/node/system.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -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,<html><body>hi</body></html>'

// 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
Expand Down
10 changes: 10 additions & 0 deletions packages/cli-kit/src/public/node/system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@ interface BuildExecOptions {
export async function openURL(url: string): Promise<boolean> {
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
Expand Down
Loading