fix: replace unguarded crypto.randomUUID() with safe generateUUID() utility

This commit is contained in:
Linus Rath
2026-04-02 14:51:21 +02:00
parent 1bdc51dcc7
commit c4673acb65
14 changed files with 38 additions and 25 deletions
+2 -2
View File
@@ -18,7 +18,7 @@ import { useContactStore, getContactDisplayName } from "@/stores/contact-store";
import { useAuthStore, redirectToLogin } from "@/stores/auth-store"; import { useAuthStore, redirectToLogin } from "@/stores/auth-store";
import { useEmailStore } from "@/stores/email-store"; import { useEmailStore } from "@/stores/email-store";
import { toast } from "@/stores/toast-store"; import { toast } from "@/stores/toast-store";
import { cn } from "@/lib/utils"; import { cn, generateUUID } from "@/lib/utils";
import { NavigationRail } from "@/components/layout/navigation-rail"; import { NavigationRail } from "@/components/layout/navigation-rail";
import { SidebarAppsModal } from "@/components/layout/sidebar-apps-modal"; import { SidebarAppsModal } from "@/components/layout/sidebar-apps-modal";
import { InlineAppView } from "@/components/layout/inline-app-view"; import { InlineAppView } from "@/components/layout/inline-app-view";
@@ -274,7 +274,7 @@ export default function ContactsPage() {
toast.success(t("toast.created")); toast.success(t("toast.created"));
} else { } else {
const localContact: ContactCard = { const localContact: ContactCard = {
id: `local-${crypto.randomUUID()}`, id: `local-${generateUUID()}`,
addressBookIds: {}, addressBookIds: {},
...data, ...data,
}; };
+2 -3
View File
@@ -20,6 +20,7 @@ import {
} from "@/lib/calendar-participants"; } from "@/lib/calendar-participants";
import { PluginSlot } from "@/components/plugins/plugin-slot"; import { PluginSlot } from "@/components/plugins/plugin-slot";
import { useSettingsStore } from "@/stores/settings-store"; import { useSettingsStore } from "@/stores/settings-store";
import { generateUUID } from "@/lib/utils";
export interface PendingEventPreview { export interface PendingEventPreview {
start: Date; start: Date;
@@ -402,9 +403,7 @@ export function EventModal({
if (!event || !onDuplicate) return; if (!event || !onDuplicate) return;
const start = getEventStartDate(event); const start = getEventStartDate(event);
const newStart = addDays(start, 1); const newStart = addDays(start, 1);
const newUid = typeof crypto !== 'undefined' && crypto.randomUUID const newUid = generateUUID();
? crypto.randomUUID()
: `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
const data: Partial<CalendarEvent> = { const data: Partial<CalendarEvent> = {
uid: newUid, uid: newUid,
title: event.title, title: event.title,
+2 -2
View File
@@ -8,7 +8,7 @@ import { EMAIL_SANITIZE_CONFIG, collapseBlockedImageContainers } from "@/lib/ema
import { hasMeaningfulHtmlBody } from "@/lib/signature-utils"; import { hasMeaningfulHtmlBody } from "@/lib/signature-utils";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Avatar } from "@/components/ui/avatar"; import { Avatar } from "@/components/ui/avatar";
import { formatFileSize, cn, buildMailboxTree, MailboxNode, formatDateTime } from "@/lib/utils"; import { formatFileSize, cn, buildMailboxTree, MailboxNode, formatDateTime, generateUUID } from "@/lib/utils";
import { getSecurityStatus, extractListHeaders } from "@/lib/email-headers"; import { getSecurityStatus, extractListHeaders } from "@/lib/email-headers";
import { import {
Reply, Reply,
@@ -4874,7 +4874,7 @@ export function EmailViewer({
if (client && supportsSync) { if (client && supportsSync) {
createContact(client, contactData).then(() => toast.success('Contact added')); createContact(client, contactData).then(() => toast.success('Contact added'));
} else { } else {
addLocalContact({ id: `local-${crypto.randomUUID()}`, addressBookIds: {}, ...contactData } as ContactCard); addLocalContact({ id: `local-${generateUUID()}`, addressBookIds: {}, ...contactData } as ContactCard);
toast.success('Contact added'); toast.success('Contact added');
} }
}} }}
+2 -2
View File
@@ -16,7 +16,7 @@ import type {
FilterActionType, FilterActionType,
} from "@/lib/jmap/sieve-types"; } from "@/lib/jmap/sieve-types";
import type { Mailbox } from "@/lib/jmap/types"; import type { Mailbox } from "@/lib/jmap/types";
import { buildMailboxTree, flattenMailboxTree, type MailboxNode } from "@/lib/utils"; import { buildMailboxTree, flattenMailboxTree, type MailboxNode, generateUUID } from "@/lib/utils";
interface FilterRuleModalProps { interface FilterRuleModalProps {
rule?: FilterRule; rule?: FilterRule;
@@ -109,7 +109,7 @@ export function FilterRuleModal({
} }
onSave({ onSave({
id: rule?.id || crypto.randomUUID(), id: rule?.id || generateUUID(),
name: trimmedName, name: trimmedName,
enabled: rule?.enabled ?? true, enabled: rule?.enabled ?? true,
matchType, matchType,
+2 -3
View File
@@ -1,4 +1,5 @@
import type { CalendarEvent, CalendarParticipant } from '@/lib/jmap/types'; import type { CalendarEvent, CalendarParticipant } from '@/lib/jmap/types';
import { generateUUID } from '@/lib/utils';
export interface ParticipantInfo { export interface ParticipantInfo {
id: string; id: string;
@@ -104,9 +105,7 @@ export function buildParticipantMap(
): Record<string, Partial<CalendarParticipant>> { ): Record<string, Partial<CalendarParticipant>> {
const participants: Record<string, Partial<CalendarParticipant>> = {}; const participants: Record<string, Partial<CalendarParticipant>> = {};
const generateId = () => typeof crypto !== 'undefined' && crypto.randomUUID const generateId = () => generateUUID();
? crypto.randomUUID()
: `p-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
participants[generateId()] = { participants[generateId()] = {
'@type': 'Participant', '@type': 'Participant',
+4 -2
View File
@@ -5,6 +5,8 @@
* All line endings are CRLF per RFC 5322. * All line endings are CRLF per RFC 5322.
*/ */
import { generateUUID } from '@/lib/utils';
const CRLF = '\r\n'; const CRLF = '\r\n';
export interface MimeAttachment { export interface MimeAttachment {
@@ -43,7 +45,7 @@ export function buildMimeMessage(input: MimeMessageInput): Uint8Array {
// BCC is intentionally omitted from the MIME headers per RFC 5322 // BCC is intentionally omitted from the MIME headers per RFC 5322
lines.push(formatHeader('Subject', encodeHeaderValue(input.subject))); lines.push(formatHeader('Subject', encodeHeaderValue(input.subject)));
lines.push(formatHeader('Date', formatDate(input.date ?? new Date()))); lines.push(formatHeader('Date', formatDate(input.date ?? new Date())));
lines.push(formatHeader('Message-ID', input.messageId ?? `<${crypto.randomUUID()}@smime.local>`)); lines.push(formatHeader('Message-ID', input.messageId ?? `<${generateUUID()}@smime.local>`));
if (input.inReplyTo) { if (input.inReplyTo) {
lines.push(formatHeader('In-Reply-To', input.inReplyTo)); lines.push(formatHeader('In-Reply-To', input.inReplyTo));
} }
@@ -245,7 +247,7 @@ export function wrapCmsAsSmimeMessage(cmsBlob: Blob | ArrayBuffer | Uint8Array,
} }
lines.push(formatHeader('Subject', encodeHeaderValue(input.subject))); lines.push(formatHeader('Subject', encodeHeaderValue(input.subject)));
lines.push(formatHeader('Date', formatDate(input.date ?? new Date()))); lines.push(formatHeader('Date', formatDate(input.date ?? new Date())));
lines.push(formatHeader('Message-ID', input.messageId ?? `<${crypto.randomUUID()}@smime.local>`)); lines.push(formatHeader('Message-ID', input.messageId ?? `<${generateUUID()}@smime.local>`));
if (input.inReplyTo) { if (input.inReplyTo) {
lines.push(formatHeader('In-Reply-To', input.inReplyTo)); lines.push(formatHeader('In-Reply-To', input.inReplyTo));
} }
+2 -1
View File
@@ -1,5 +1,6 @@
import * as asn1js from 'asn1js'; import * as asn1js from 'asn1js';
import * as pkijs from 'pkijs'; import * as pkijs from 'pkijs';
import { generateUUID } from '@/lib/utils';
import { import {
extractCertificateInfo, extractCertificateInfo,
classifyCapabilities, classifyCapabilities,
@@ -154,7 +155,7 @@ export async function importPkcs12(
const email = certInfo.emailAddresses[0] ?? ''; const email = certInfo.emailAddresses[0] ?? '';
const keyRecord: SmimeKeyRecord = { const keyRecord: SmimeKeyRecord = {
id: crypto.randomUUID(), id: generateUUID(),
email: email.toLowerCase(), email: email.toLowerCase(),
certificate: leafCertDer, certificate: leafCertDer,
certificateChain: chainCertsDer, certificateChain: chainCertsDer,
+2 -1
View File
@@ -1,6 +1,7 @@
import DOMPurify from 'dompurify'; import DOMPurify from 'dompurify';
import type { EmailTemplate } from './template-types'; import type { EmailTemplate } from './template-types';
import { BUILT_IN_PLACEHOLDERS } from './template-types'; import { BUILT_IN_PLACEHOLDERS } from './template-types';
import { generateUUID } from './utils';
const PLACEHOLDER_REGEX = /\{\{(\w+)\}\}/g; const PLACEHOLDER_REGEX = /\{\{(\w+)\}\}/g;
const MAX_TEMPLATE_NAME_LENGTH = 200; const MAX_TEMPLATE_NAME_LENGTH = 200;
@@ -149,7 +150,7 @@ export function importTemplates(json: string): ImportResult {
const recipients = t.defaultRecipients as Record<string, unknown> | undefined; const recipients = t.defaultRecipients as Record<string, unknown> | undefined;
templates.push({ templates.push({
id: crypto.randomUUID(), id: generateUUID(),
name: sanitizeText(t.name), name: sanitizeText(t.name),
subject: sanitizeText(t.subject), subject: sanitizeText(t.subject),
body: sanitizeText(t.body), body: sanitizeText(t.body),
+7
View File
@@ -7,6 +7,13 @@ export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)); return twMerge(clsx(inputs));
} }
export function generateUUID(): string {
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
return crypto.randomUUID();
}
return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
}
export function formatDate(date: Date | string): string { export function formatDate(date: Date | string): string {
const d = typeof date === "string" ? new Date(date) : date; const d = typeof date === "string" ? new Date(date) : date;
const now = new Date(); const now = new Date();
+2 -1
View File
@@ -1,4 +1,5 @@
import type { ContactCard, NameComponent, ContactOnlineService, AnniversaryDate, PartialDate } from "@/lib/jmap/types"; import type { ContactCard, NameComponent, ContactOnlineService, AnniversaryDate, PartialDate } from "@/lib/jmap/types";
import { generateUUID } from "@/lib/utils";
// Convert RFC 9553 AnniversaryDate (PartialDate|Timestamp|string) to vCard date string // Convert RFC 9553 AnniversaryDate (PartialDate|Timestamp|string) to vCard date string
function anniversaryDateToVcardString(date: AnniversaryDate): string { function anniversaryDateToVcardString(date: AnniversaryDate): string {
@@ -153,7 +154,7 @@ export function parseVCard(vcfString: string): ContactCard[] {
} }
function buildContact(raw: Record<string, string[]>): ContactCard | null { function buildContact(raw: Record<string, string[]>): ContactCard | null {
const id = `import-${crypto.randomUUID()}`; const id = `import-${generateUUID()}`;
const card: ContactCard = { id, addressBookIds: {} }; const card: ContactCard = { id, addressBookIds: {} };
for (const [fullKey, values] of Object.entries(raw)) { for (const [fullKey, values] of Object.entries(raw)) {
+2 -3
View File
@@ -6,6 +6,7 @@ import { debug } from '@/lib/debug';
import { normalizeAllDayDuration } from '@/lib/calendar-utils'; import { normalizeAllDayDuration } from '@/lib/calendar-utils';
import { sanitizeOutgoingCalendarEventData } from '@/lib/calendar-event-normalization'; import { sanitizeOutgoingCalendarEventData } from '@/lib/calendar-event-normalization';
import { expandRecurringEvents } from '@/lib/recurrence-expansion'; import { expandRecurringEvents } from '@/lib/recurrence-expansion';
import { generateUUID } from '@/lib/utils';
export type CalendarViewMode = 'month' | 'week' | 'day' | 'agenda' | 'tasks'; export type CalendarViewMode = 'month' | 'week' | 'day' | 'agenda' | 'tasks';
@@ -722,9 +723,7 @@ export const useCalendarStore = create<CalendarStore>()(
if (!calendar) throw new Error('Failed to create calendar'); if (!calendar) throw new Error('Failed to create calendar');
const subscription: ICalSubscription = { const subscription: ICalSubscription = {
id: typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function' id: generateUUID(),
? crypto.randomUUID()
: `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`,
url, url,
calendarId: calendar.id, calendarId: calendar.id,
name, name,
+3 -2
View File
@@ -2,6 +2,7 @@ import { create } from 'zustand';
import { persist } from 'zustand/middleware'; import { persist } from 'zustand/middleware';
import type { ContactCard, AddressBook, ContactName } from '@/lib/jmap/types'; import type { ContactCard, AddressBook, ContactName } from '@/lib/jmap/types';
import type { IJMAPClient } from '@/lib/jmap/client-interface'; import type { IJMAPClient } from '@/lib/jmap/client-interface';
import { generateUUID } from '@/lib/utils';
export function getContactDisplayName(contact: ContactCard): string { export function getContactDisplayName(contact: ContactCard): string {
if (contact.name?.components) { if (contact.name?.components) {
@@ -378,7 +379,7 @@ export const useContactStore = create<ContactStore>()(
set((state) => ({ contacts: [...state.contacts, created] })); set((state) => ({ contacts: [...state.contacts, created] }));
} else { } else {
const localGroup: ContactCard = { const localGroup: ContactCard = {
id: `local-${crypto.randomUUID()}`, id: `local-${generateUUID()}`,
addressBookIds: {}, addressBookIds: {},
...groupData, ...groupData,
} as ContactCard; } as ContactCard;
@@ -617,7 +618,7 @@ export const useContactStore = create<ContactStore>()(
} else { } else {
const localContact: ContactCard = { const localContact: ContactCard = {
...contact, ...contact,
id: `local-${crypto.randomUUID()}`, id: `local-${generateUUID()}`,
}; };
set((state) => ({ contacts: [...state.contacts, localContact] })); set((state) => ({ contacts: [...state.contacts, localContact] }));
} }
+2 -1
View File
@@ -1,6 +1,7 @@
import { create } from 'zustand'; import { create } from 'zustand';
import { persist } from 'zustand/middleware'; import { persist } from 'zustand/middleware';
import type { SmimeKeyRecord, SmimePublicCert } from '@/lib/smime/types'; import type { SmimeKeyRecord, SmimePublicCert } from '@/lib/smime/types';
import { generateUUID } from '@/lib/utils';
import { import {
saveKeyRecord, saveKeyRecord,
listKeyRecords, listKeyRecords,
@@ -287,7 +288,7 @@ export const useSmimeStore = create<SmimeStore>()(
const email = info.emailAddresses[0] ?? ''; const email = info.emailAddresses[0] ?? '';
const publicCert: SmimePublicCert = { const publicCert: SmimePublicCert = {
id: crypto.randomUUID(), id: generateUUID(),
accountId: get().currentAccountId ?? undefined, accountId: get().currentAccountId ?? undefined,
email: email.toLowerCase(), email: email.toLowerCase(),
certificate: der, certificate: der,
+4 -2
View File
@@ -6,6 +6,7 @@ import {
importTemplates as importUtil, importTemplates as importUtil,
filterTemplates, filterTemplates,
} from '@/lib/template-utils'; } from '@/lib/template-utils';
import { generateUUID } from '@/lib/utils';
const MAX_RECENT = 5; const MAX_RECENT = 5;
@@ -39,7 +40,7 @@ export const useTemplateStore = create<TemplateStore>()(
const now = new Date().toISOString(); const now = new Date().toISOString();
const template: EmailTemplate = { const template: EmailTemplate = {
...data, ...data,
id: crypto.randomUUID(), id: generateUUID(),
createdAt: now, createdAt: now,
updatedAt: now, updatedAt: now,
}; };
@@ -73,8 +74,9 @@ export const useTemplateStore = create<TemplateStore>()(
const now = new Date().toISOString(); const now = new Date().toISOString();
const duplicate: EmailTemplate = { const duplicate: EmailTemplate = {
...original, ...original,
id: crypto.randomUUID(), id: generateUUID(),
name: `${original.name} ${nameSuffix || '(copy)'}`, name: `${original.name} ${nameSuffix || '(copy)'}`,
isFavorite: false, isFavorite: false,
createdAt: now, createdAt: now,
updatedAt: now, updatedAt: now,