From 7c508f740fb8e8f4c5651bacaf7439a11bd2979e Mon Sep 17 00:00:00 2001 From: gonzaloriestra <14979109+gonzaloriestra@users.noreply.github.com> Date: Sun, 26 Jul 2026 00:21:13 +0000 Subject: [PATCH] [Security] Harden client ID generation in ExtensionServerClient Uses globalThis.crypto.randomUUID when available to leverage a CSPRNG, fallback to Math.random otherwise. --- .../ExtensionServerClient.ts | 4 +- .../src/utilities/index.ts | 1 + .../src/utilities/randomId.test.ts | 64 +++++++++++++++++++ .../src/utilities/randomId.ts | 11 ++++ 4 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 packages/ui-extensions-server-kit/src/utilities/randomId.test.ts create mode 100644 packages/ui-extensions-server-kit/src/utilities/randomId.ts diff --git a/packages/ui-extensions-server-kit/src/ExtensionServerClient/ExtensionServerClient.ts b/packages/ui-extensions-server-kit/src/ExtensionServerClient/ExtensionServerClient.ts index 507bfb5df73..f3d2eab56f2 100644 --- a/packages/ui-extensions-server-kit/src/ExtensionServerClient/ExtensionServerClient.ts +++ b/packages/ui-extensions-server-kit/src/ExtensionServerClient/ExtensionServerClient.ts @@ -8,7 +8,7 @@ import { getFlattenedLocalization, isFlattenedTranslations, } from '../i18n' -import {isUIExtension, isValidSurface} from '../utilities' +import {isUIExtension, isValidSurface, generateRandomId} from '../utilities' import {DeepPartial, ExtensionPayload, ExtensionPoint} from '../types' export class ExtensionServerClient implements ExtensionServer.Client { @@ -32,7 +32,7 @@ export class ExtensionServerClient implements ExtensionServer.Client { private uiExtensionsByUuid: Record = {} constructor(options: DeepPartial = {}) { - this.id = (Math.random() + 1).toString(36).substring(7) + this.id = generateRandomId() this.options = getValidatedOptions({ ...options, connection: { diff --git a/packages/ui-extensions-server-kit/src/utilities/index.ts b/packages/ui-extensions-server-kit/src/utilities/index.ts index f53050a3aa8..740d379f1e8 100644 --- a/packages/ui-extensions-server-kit/src/utilities/index.ts +++ b/packages/ui-extensions-server-kit/src/utilities/index.ts @@ -4,3 +4,4 @@ export * from './set' export * from './assetToString' export * from './isValidSurface' export * from './isUIExtension' +export * from './randomId' diff --git a/packages/ui-extensions-server-kit/src/utilities/randomId.test.ts b/packages/ui-extensions-server-kit/src/utilities/randomId.test.ts new file mode 100644 index 00000000000..1eec72be84a --- /dev/null +++ b/packages/ui-extensions-server-kit/src/utilities/randomId.test.ts @@ -0,0 +1,64 @@ +import {generateRandomId} from './randomId' +import {describe, test, expect, vi, afterEach} from 'vitest' + +describe('generateRandomId', () => { + const originalCrypto = globalThis.crypto + + afterEach(() => { + // Restore global crypto + Object.defineProperty(globalThis, 'crypto', { + value: originalCrypto, + writable: true, + configurable: true, + }) + // eslint-disable-next-line @shopify/cli/no-vi-manual-mock-clear + vi.restoreAllMocks() + }) + + test('uses globalThis.crypto.randomUUID when available', () => { + const mockUUID = '12345678-1234-1234-1234-123456789abc' + const mockRandomUUID = vi.fn().mockReturnValue(mockUUID) + + Object.defineProperty(globalThis, 'crypto', { + value: { + randomUUID: mockRandomUUID, + }, + writable: true, + configurable: true, + }) + + const id = generateRandomId() + expect(id).toBe(mockUUID) + expect(mockRandomUUID).toHaveBeenCalledTimes(1) + }) + + test('falls back to Math.random when crypto is unavailable', () => { + Object.defineProperty(globalThis, 'crypto', { + value: undefined, + writable: true, + configurable: true, + }) + + const mockRandom = vi.spyOn(Math, 'random').mockReturnValue(0.123456) + + const id = generateRandomId() + expect(id).toBeDefined() + expect(typeof id).toBe('string') + expect(mockRandom).toHaveBeenCalled() + }) + + test('falls back to Math.random when randomUUID is not a function', () => { + Object.defineProperty(globalThis, 'crypto', { + value: {}, + writable: true, + configurable: true, + }) + + const mockRandom = vi.spyOn(Math, 'random').mockReturnValue(0.123456) + + const id = generateRandomId() + expect(id).toBeDefined() + expect(typeof id).toBe('string') + expect(mockRandom).toHaveBeenCalled() + }) +}) diff --git a/packages/ui-extensions-server-kit/src/utilities/randomId.ts b/packages/ui-extensions-server-kit/src/utilities/randomId.ts new file mode 100644 index 00000000000..5cb5b217889 --- /dev/null +++ b/packages/ui-extensions-server-kit/src/utilities/randomId.ts @@ -0,0 +1,11 @@ +/** + * Generates a random identifier. + * Uses `globalThis.crypto.randomUUID` to provide a cryptographically secure pseudo-random number generator (CSPRNG) + * and prevent predictable IDs/token vulnerability, with a fallback to `Math.random()` for non-secure contexts. + */ +export function generateRandomId(): string { + if (typeof globalThis !== 'undefined' && globalThis.crypto && typeof globalThis.crypto.randomUUID === 'function') { + return globalThis.crypto.randomUUID() + } + return (Math.random() + 1).toString(36).substring(7) +}