diff --git a/components/email/email-composer.tsx b/components/email/email-composer.tsx index df932fb0..c1ab9759 100644 --- a/components/email/email-composer.tsx +++ b/components/email/email-composer.tsx @@ -9,7 +9,7 @@ import { X, Paperclip, Send, Save, Check, Loader2, AlertCircle, FileText, Bookma import { cn, formatFileSize, formatDateTime, generateUUID } from "@/lib/utils"; import { debug } from "@/lib/debug"; import { toast } from "@/stores/toast-store"; -import { sanitizeEmailHtml } from "@/lib/email-sanitization"; +import { sanitizeSignatureHtml } from "@/lib/email-sanitization"; import { emailHooks, contactHooks } from "@/lib/plugin-hooks"; import type { OutgoingEmail, RecipientSuggestion } from "@/lib/plugin-types"; import { useAuthStore } from "@/stores/auth-store"; @@ -137,7 +137,7 @@ function buildEmbeddedSignatureHtml( : `

`; const endMarker = `

`; if (identity?.htmlSignature) { - return `${startMarker}${sanitizeEmailHtml(identity.htmlSignature)}${endMarker}`; + return `${startMarker}${sanitizeSignatureHtml(identity.htmlSignature)}${endMarker}`; } if (identity?.textSignature) { const escaped = identity.textSignature @@ -471,7 +471,7 @@ export function EmailComposer({ ]); const composerSignatureHtml = signatureIdentity?.htmlSignature - ? `
${sanitizeEmailHtml(signatureIdentity.htmlSignature)}
` + ? `
${sanitizeSignatureHtml(signatureIdentity.htmlSignature)}
` : signatureIdentity?.textSignature ? `
${getPlainTextSignature(signatureIdentity).replace(/&/g, '&').replace(//g, '>').replace(/\n/g, '
')}
` : ''; @@ -1128,7 +1128,7 @@ export function EmailComposer({ if (signatureAlreadyInBody) return ''; const sep = signatureSeparatorEnabled ? `

--
` : `

`; if (signatureIdentity?.htmlSignature) { - return `${sep}${sanitizeEmailHtml(signatureIdentity.htmlSignature)}`; + return `${sep}${sanitizeSignatureHtml(signatureIdentity.htmlSignature)}`; } if (signatureIdentity?.textSignature) { return `${sep}${signatureIdentity.textSignature.replace(/&/g, '&').replace(//g, '>').replace(/\n/g, '
')}`; diff --git a/components/email/email-viewer.tsx b/components/email/email-viewer.tsx index 573acbf3..595dc533 100644 --- a/components/email/email-viewer.tsx +++ b/components/email/email-viewer.tsx @@ -3,7 +3,7 @@ import { useState, useEffect, useLayoutEffect, useMemo, useRef, useCallback } from "react"; import DOMPurify from "dompurify"; import { Email, ContactCard, Mailbox } from "@/lib/jmap/types"; -import { EMAIL_IFRAME_SANITIZE_CONFIG, collapseBlockedImageContainers, escapeHtml, plainTextToSafeHtml, sanitizeEmailHtml } from "@/lib/email-sanitization"; +import { EMAIL_IFRAME_SANITIZE_CONFIG, collapseBlockedImageContainers, escapeHtml, plainTextToSafeHtml, sanitizeEmailHtml, sanitizePlainTextRenderedHtml } from "@/lib/email-sanitization"; import { hasMeaningfulHtmlBody } from "@/lib/signature-utils"; import { Button } from "@/components/ui/button"; import { Avatar } from "@/components/ui/avatar"; @@ -5178,7 +5178,7 @@ export function EmailViewer({ ) : (
)}
diff --git a/components/settings/account-security-settings.tsx b/components/settings/account-security-settings.tsx index 571dccb3..2c9d0e11 100644 --- a/components/settings/account-security-settings.tsx +++ b/components/settings/account-security-settings.tsx @@ -12,6 +12,7 @@ import { useAccountSecurityStore, type AppPasswordInfo, type ApiKeyInfo, type Ap import { useAuthStore } from '@/stores/auth-store'; import { toast } from '@/stores/toast-store'; import { cn } from '@/lib/utils'; +import { sanitizeI18nHtml } from '@/lib/email-sanitization'; function PasswordChangeSection() { const t = useTranslations('settings.security'); @@ -671,7 +672,7 @@ export function AccountSecuritySettings() { if (isStalwart === false) { return ( -
+
); } diff --git a/lib/email-sanitization.ts b/lib/email-sanitization.ts index c48c06bd..f1452c98 100644 --- a/lib/email-sanitization.ts +++ b/lib/email-sanitization.ts @@ -12,9 +12,12 @@ export const EMAIL_SANITIZE_CONFIG = { ALLOW_DATA_ATTR: false, FORCE_BODY: true, // Allow blob: URIs so authenticated inline images (CID) are not stripped. - // data: is restricted to image/* MIME types to prevent SVG script injection. + // data: is restricted to a fixed set of raster image types. SVG (image/svg+xml) + // is excluded because DOMPurify cannot inspect bytes inside a data: URI, so an + // SVG payload can carry