Skip to content
Merged
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
@@ -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"
}
4 changes: 2 additions & 2 deletions web-frontend/modules/core/runtimeFormulaTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -1666,7 +1666,7 @@ export class RuntimeGenerateUUID extends RuntimeFormulaFunction {
}

execute(context, args) {
return crypto.randomUUID()
return generateUUID()
}

getDescription() {
Expand Down
36 changes: 36 additions & 0 deletions web-frontend/modules/core/utils/string.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
4 changes: 3 additions & 1 deletion web-frontend/modules/database/utils/action.js
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
63 changes: 63 additions & 0 deletions web-frontend/test/unit/core/utils/string.spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
uuid,
generateUUID,
upperCaseFirst,
slugify,
isValidURL,
Expand All @@ -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')
Expand Down
Loading