From 3ceada7b8ab23392ef4925bd96e3dc49f6d83fa9 Mon Sep 17 00:00:00 2001
From: Linus Rath <139418639+rathlinus@users.noreply.github.com>
Date: Mon, 18 May 2026 16:07:34 +0200
Subject: [PATCH] fix: tighten HTML sanitization at plain-text email +
signature + i18n render sites
---
components/email/email-composer.tsx | 8 ++--
components/email/email-viewer.tsx | 4 +-
components/email/thread-conversation-view.tsx | 4 +-
.../settings/account-security-settings.tsx | 3 +-
lib/email-sanitization.ts | 43 ++++++++++++++++++-
5 files changed, 51 insertions(+), 11 deletions(-)
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