From 81ca2df6ad57085b895caafc386e4ac4ab9098a6 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Wed, 11 Feb 2026 19:05:15 -0600 Subject: fix(app): guard randomUUID in insecure browser contexts (#13237) Co-authored-by: Selim <31136147+selimerunkut@users.noreply.github.com> --- packages/app/src/utils/perf.ts | 8 ++-- packages/app/src/utils/uuid.test.ts | 78 +++++++++++++++++++++++++++++++++++++ packages/app/src/utils/uuid.ts | 12 ++++++ 3 files changed, 94 insertions(+), 4 deletions(-) create mode 100644 packages/app/src/utils/uuid.test.ts create mode 100644 packages/app/src/utils/uuid.ts (limited to 'packages/app/src/utils') diff --git a/packages/app/src/utils/perf.ts b/packages/app/src/utils/perf.ts index 0ecbc33ff..105d02a82 100644 --- a/packages/app/src/utils/perf.ts +++ b/packages/app/src/utils/perf.ts @@ -1,3 +1,5 @@ +import { uuid } from "@/utils/uuid" + type Nav = { id: string dir?: string @@ -16,8 +18,6 @@ const key = (dir: string | undefined, to: string) => `${dir ?? ""}:${to}` const now = () => performance.now() -const uid = () => crypto.randomUUID?.() ?? Math.random().toString(16).slice(2) - const navs = new Map() const pending = new Map() const active = new Map() @@ -94,7 +94,7 @@ function ensure(id: string, data: Omit) { export function navStart(input: { dir?: string; from?: string; to: string; trigger?: string }) { if (!dev) return - const id = uid() + const id = uuid() const start = now() const nav = ensure(id, { ...input, id, start }) nav.marks["navigate:start"] = start @@ -109,7 +109,7 @@ export function navParams(input: { dir?: string; from?: string; to: string }) { const k = key(input.dir, input.to) const pendingId = pending.get(k) if (pendingId) pending.delete(k) - const id = pendingId ?? uid() + const id = pendingId ?? uuid() const start = now() const nav = ensure(id, { ...input, id, start, trigger: pendingId ? "key" : "route" }) diff --git a/packages/app/src/utils/uuid.test.ts b/packages/app/src/utils/uuid.test.ts new file mode 100644 index 000000000..e6b4e2824 --- /dev/null +++ b/packages/app/src/utils/uuid.test.ts @@ -0,0 +1,78 @@ +import { afterEach, describe, expect, test } from "bun:test" +import { uuid } from "./uuid" + +const cryptoDescriptor = Object.getOwnPropertyDescriptor(globalThis, "crypto") +const secureDescriptor = Object.getOwnPropertyDescriptor(globalThis, "isSecureContext") +const randomDescriptor = Object.getOwnPropertyDescriptor(Math, "random") + +const setCrypto = (value: Partial) => { + Object.defineProperty(globalThis, "crypto", { + configurable: true, + value: value as Crypto, + }) +} + +const setSecure = (value: boolean) => { + Object.defineProperty(globalThis, "isSecureContext", { + configurable: true, + value, + }) +} + +const setRandom = (value: () => number) => { + Object.defineProperty(Math, "random", { + configurable: true, + value, + }) +} + +afterEach(() => { + if (cryptoDescriptor) { + Object.defineProperty(globalThis, "crypto", cryptoDescriptor) + } + + if (secureDescriptor) { + Object.defineProperty(globalThis, "isSecureContext", secureDescriptor) + } + + if (!secureDescriptor) { + delete (globalThis as { isSecureContext?: boolean }).isSecureContext + } + + if (randomDescriptor) { + Object.defineProperty(Math, "random", randomDescriptor) + } +}) + +describe("uuid", () => { + test("uses randomUUID in secure contexts", () => { + setCrypto({ randomUUID: () => "00000000-0000-0000-0000-000000000000" }) + setSecure(true) + expect(uuid()).toBe("00000000-0000-0000-0000-000000000000") + }) + + test("falls back in insecure contexts", () => { + setCrypto({ randomUUID: () => "00000000-0000-0000-0000-000000000000" }) + setSecure(false) + setRandom(() => 0.5) + expect(uuid()).toBe("8") + }) + + test("falls back when randomUUID throws", () => { + setCrypto({ + randomUUID: () => { + throw new DOMException("Failed", "OperationError") + }, + }) + setSecure(true) + setRandom(() => 0.5) + expect(uuid()).toBe("8") + }) + + test("falls back when randomUUID is unavailable", () => { + setCrypto({}) + setSecure(true) + setRandom(() => 0.5) + expect(uuid()).toBe("8") + }) +}) diff --git a/packages/app/src/utils/uuid.ts b/packages/app/src/utils/uuid.ts new file mode 100644 index 000000000..7b964068c --- /dev/null +++ b/packages/app/src/utils/uuid.ts @@ -0,0 +1,12 @@ +const fallback = () => Math.random().toString(16).slice(2) + +export function uuid() { + const c = globalThis.crypto + if (!c || typeof c.randomUUID !== "function") return fallback() + if (typeof globalThis.isSecureContext === "boolean" && !globalThis.isSecureContext) return fallback() + try { + return c.randomUUID() + } catch { + return fallback() + } +} -- cgit v1.2.3