From a615b870bc79c669d31a51d1f42147615a063c6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Pardou?= <571533+jrmi@users.noreply.github.com> Date: Wed, 25 Mar 2026 08:52:08 +0100 Subject: [PATCH] =?UTF-8?q?fix:=20switch=20to=20JS=E2=80=AFgenerated=20UUI?= =?UTF-8?q?Dv4=20for=20insecure=20context=20(#5032)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...rnative_for_uuid_generation_when_on_u.json | 9 +++ .../modules/core/runtimeFormulaTypes.js | 4 +- web-frontend/modules/core/utils/string.js | 36 +++++++++++ web-frontend/modules/database/utils/action.js | 4 +- .../test/unit/core/utils/string.spec.js | 63 +++++++++++++++++++ 5 files changed, 113 insertions(+), 3 deletions(-) create mode 100644 changelog/entries/unreleased/bug/4905_switch_to_unsecure_alternative_for_uuid_generation_when_on_u.json diff --git a/changelog/entries/unreleased/bug/4905_switch_to_unsecure_alternative_for_uuid_generation_when_on_u.json b/changelog/entries/unreleased/bug/4905_switch_to_unsecure_alternative_for_uuid_generation_when_on_u.json new file mode 100644 index 0000000000..4ec8a8d118 --- /dev/null +++ b/changelog/entries/unreleased/bug/4905_switch_to_unsecure_alternative_for_uuid_generation_when_on_u.json @@ -0,0 +1,9 @@ +{ + "type": "bug", + "message": "Switch to insecure alternative for UUID generation when on insecure context like HTTP", + "issue_origin": "github", + "issue_number": 4905, + "domain": "core", + "bullet_points": [], + "created_at": "2026-03-23" +} diff --git a/web-frontend/modules/core/runtimeFormulaTypes.js b/web-frontend/modules/core/runtimeFormulaTypes.js index 0def43c347..04ce00f4af 100644 --- a/web-frontend/modules/core/runtimeFormulaTypes.js +++ b/web-frontend/modules/core/runtimeFormulaTypes.js @@ -13,7 +13,7 @@ import { InvalidFormulaArgumentType, InvalidNumberOfArguments, } from '@baserow/modules/core/formula/parser/errors' -import { reverseString } from '@baserow/modules/core/utils/string' +import { reverseString, generateUUID } from '@baserow/modules/core/utils/string' import { avg, sum } from '@baserow/modules/core/utils/number' import { ensureString, @@ -1666,7 +1666,7 @@ export class RuntimeGenerateUUID extends RuntimeFormulaFunction { } execute(context, args) { - return crypto.randomUUID() + return generateUUID() } getDescription() { diff --git a/web-frontend/modules/core/utils/string.js b/web-frontend/modules/core/utils/string.js index dc1ba622bd..b86cd7d49f 100644 --- a/web-frontend/modules/core/utils/string.js +++ b/web-frontend/modules/core/utils/string.js @@ -20,6 +20,42 @@ export const uuid = function () { return uuid } +export function generateUUID() { + if ( + typeof crypto !== 'undefined' && + typeof crypto?.randomUUID === 'function' + ) { + return crypto.randomUUID() + } + + if ( + typeof crypto !== 'undefined' && + typeof crypto?.getRandomValues === 'function' + ) { + const bytes = new Uint8Array(16) + crypto.getRandomValues(bytes) + + bytes[6] = (bytes[6] & 0x0f) | 0x40 + bytes[8] = (bytes[8] & 0x3f) | 0x80 + + const hex = Array.from(bytes, (byte) => + byte.toString(16).padStart(2, '0') + ).join('') + + return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice( + 12, + 16 + )}-${hex.slice(16, 20)}-${hex.slice(20)}` + } + + // fallback (non-crypto) + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { + const r = (Math.random() * 16) | 0 + const v = c === 'x' ? r : (r & 0x3) | 0x8 + return v.toString(16) + }) +} + /** * Generate a random string for small UID with low chances of collision. */ diff --git a/web-frontend/modules/database/utils/action.js b/web-frontend/modules/database/utils/action.js index e89cd7d4e4..4dbc188b03 100644 --- a/web-frontend/modules/database/utils/action.js +++ b/web-frontend/modules/database/utils/action.js @@ -1,5 +1,7 @@ +import { generateUUID } from '@baserow/modules/core/utils/string' + export const createNewUndoRedoActionGroupId = () => { - return crypto.randomUUID() + return generateUUID() } export const UNDO_REDO_ACTION_GROUP_HEADER = 'ClientUndoRedoActionGroupId' diff --git a/web-frontend/test/unit/core/utils/string.spec.js b/web-frontend/test/unit/core/utils/string.spec.js index 13bdb4351d..400769b273 100644 --- a/web-frontend/test/unit/core/utils/string.spec.js +++ b/web-frontend/test/unit/core/utils/string.spec.js @@ -1,5 +1,6 @@ import { uuid, + generateUUID, upperCaseFirst, slugify, isValidURL, @@ -11,11 +12,73 @@ import { } from '@baserow/modules/core/utils/string' describe('test string utils', () => { + const originalCrypto = globalThis.crypto + const uuidV4Pattern = + /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i + + afterEach(() => { + Object.defineProperty(globalThis, 'crypto', { + value: originalCrypto, + configurable: true, + writable: true, + }) + vi.restoreAllMocks() + }) + test('uuid', () => { const value = uuid() expect(typeof value).toBe('string') }) + test('generateUUID uses crypto.randomUUID when available', () => { + const randomUUID = vi.fn(() => 'test-random-uuid') + const getRandomValues = vi.fn() + + Object.defineProperty(globalThis, 'crypto', { + value: { randomUUID, getRandomValues }, + configurable: true, + writable: true, + }) + + expect(generateUUID()).toBe('test-random-uuid') + expect(randomUUID).toHaveBeenCalledTimes(1) + expect(getRandomValues).not.toHaveBeenCalled() + }) + + test('generateUUID uses crypto.getRandomValues when randomUUID is unavailable', () => { + const getRandomValues = vi.fn((bytes) => { + bytes.set([ + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, + 0xcc, 0xdd, 0xee, 0xff, + ]) + return bytes + }) + + Object.defineProperty(globalThis, 'crypto', { + value: { getRandomValues }, + configurable: true, + writable: true, + }) + + expect(generateUUID()).toBe('00112233-4455-4677-8899-aabbccddeeff') + expect(getRandomValues).toHaveBeenCalledTimes(1) + }) + + test('generateUUID falls back to Math.random when crypto is unavailable', () => { + Object.defineProperty(globalThis, 'crypto', { + value: undefined, + configurable: true, + writable: true, + }) + + const mathRandomSpy = vi.spyOn(Math, 'random') + + const value = generateUUID() + + expect(value).toMatch(uuidV4Pattern) + expect(mathRandomSpy).toHaveBeenCalled() + }) + test('upperCaseFirst', () => { expect(upperCaseFirst('test string')).toBe('Test string') expect(upperCaseFirst('Test string')).toBe('Test string')