From 0e6bdd339b8303d7efaf6c7e0805eaeb0963a5fa Mon Sep 17 00:00:00 2001 From: gonzaloriestra <14979109+gonzaloriestra@users.noreply.github.com> Date: Sun, 19 Jul 2026 00:28:58 +0000 Subject: [PATCH] Harden ExtensionServerClient random ID generation Use 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 | 40 +++++++++++++++++++ .../src/utilities/randomId.ts | 11 +++++ 4 files changed, 54 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..832b62994fa --- /dev/null +++ b/packages/ui-extensions-server-kit/src/utilities/randomId.test.ts @@ -0,0 +1,40 @@ +import {generateRandomId} from './randomId' +import {describe, test, expect, vi, afterEach} from 'vitest' + +describe('generateRandomId', () => { + afterEach(() => { + // 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-123456789012' + const randomUUIDSpy = vi.spyOn(globalThis.crypto, 'randomUUID').mockReturnValue(mockUUID) + + const id = generateRandomId() + + expect(id).toBe(mockUUID) + expect(randomUUIDSpy).toHaveBeenCalledTimes(1) + }) + + test('falls back to Math.random() when globalThis.crypto.randomUUID is not available', () => { + const originalCrypto = globalThis.crypto + const originalRandomUUID = globalThis.crypto?.randomUUID + + if (globalThis.crypto) { + ;(globalThis.crypto as any).randomUUID = undefined + } + + const mathRandomSpy = vi.spyOn(Math, 'random').mockReturnValue(0.123456789) + + const id = generateRandomId() + + expect(typeof id).toBe('string') + expect(id.length).toBeGreaterThan(0) + expect(mathRandomSpy).toHaveBeenCalled() + + if (originalCrypto) { + ;(originalCrypto as any).randomUUID = originalRandomUUID + } + }) +}) 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) +}