From 82f376a0647ddaff9442cf5fc845b1cfb38a4aab Mon Sep 17 00:00:00 2001 From: gonzaloriestra <14979109+gonzaloriestra@users.noreply.github.com> Date: Wed, 20 May 2026 01:19:19 +0000 Subject: [PATCH 1/2] [Security] Harden openURL protocol validation Restrict allowed protocols in `openURL` to `http:`, `https:`, and `file:`. This prevents the execution of dangerous protocols like `javascript:` or `data:` via the system's default opener. - Added protocol validation using `URL` constructor in `packages/cli-kit/src/public/node/system.ts`. - Added comprehensive regression tests in `packages/cli-kit/src/public/node/system.test.ts`. - Updated `.jules/sentinel.md` with critical learnings. --- .jules/sentinel.md | 4 + .../cli-kit/src/public/node/system.test.ts | 76 +++++++++++++++++++ packages/cli-kit/src/public/node/system.ts | 10 +++ 3 files changed, 90 insertions(+) create mode 100644 .jules/sentinel.md diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 0000000000..4a82dd5440 --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,4 @@ +## 2026-05-20 - Protocol validation in system openers +**Vulnerability:** The `openURL` function allowed any URL protocol to be passed to the system's default opener (via the `open` library). This could allow execution of dangerous protocols like `javascript:` or `data:` if an attacker could influence the URL passed to this function. +**Learning:** System openers often handle a wide variety of protocols beyond `http:` and `https:`. In a CLI context where some URLs might be constructed from external data (e.g., from a CDN or API), it's important to restrict allowed protocols at the trust boundary. +**Prevention:** Implement strict allowlists for URL protocols in functions that trigger external actions like opening a browser or executing a command. 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