fix: prevent XSS via quote injection in plain-text email linkifier
This commit is contained in:
@@ -4,7 +4,7 @@ import { useState, useEffect, useLayoutEffect, useMemo, useRef, useCallback } fr
|
||||
import ReactDOM from "react-dom";
|
||||
import DOMPurify from "dompurify";
|
||||
import { Email, ContactCard, Mailbox } from "@/lib/jmap/types";
|
||||
import { EMAIL_SANITIZE_CONFIG, collapseBlockedImageContainers } from "@/lib/email-sanitization";
|
||||
import { EMAIL_SANITIZE_CONFIG, collapseBlockedImageContainers, plainTextToSafeHtml } from "@/lib/email-sanitization";
|
||||
import { hasMeaningfulHtmlBody } from "@/lib/signature-utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Avatar } from "@/components/ui/avatar";
|
||||
@@ -2397,16 +2397,8 @@ export function EmailViewer({
|
||||
if (email.textBody?.[0]?.partId && email.bodyValues[email.textBody[0].partId]) {
|
||||
const textContent = email.bodyValues[email.textBody[0].partId].value;
|
||||
|
||||
// Convert plain text to HTML with proper formatting
|
||||
// Uses white-space: pre-wrap on the container to preserve newlines/whitespace
|
||||
const htmlFromText = textContent
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/(https?:\/\/[^\s<]+)/g, '<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>');
|
||||
|
||||
return {
|
||||
html: htmlFromText,
|
||||
html: plainTextToSafeHtml(textContent),
|
||||
isHtml: false
|
||||
};
|
||||
}
|
||||
@@ -2444,12 +2436,7 @@ export function EmailViewer({
|
||||
return { html: cleanHtml, isHtml: true };
|
||||
}
|
||||
if (smimeDecryptedText) {
|
||||
const htmlFromText = smimeDecryptedText
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/(https?:\/\/[^\s<]+)/g, '<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>');
|
||||
return { html: htmlFromText, isHtml: false };
|
||||
return { html: plainTextToSafeHtml(smimeDecryptedText), isHtml: false };
|
||||
}
|
||||
// TNEF (winmail.dat) extracted content
|
||||
if (tnefHtml) {
|
||||
@@ -2457,12 +2444,7 @@ export function EmailViewer({
|
||||
return { html: cleanHtml, isHtml: true };
|
||||
}
|
||||
if (tnefText) {
|
||||
const htmlFromText = tnefText
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/(https?:\/\/[^\s<]+)/g, '<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>');
|
||||
return { html: htmlFromText, isHtml: false };
|
||||
return { html: plainTextToSafeHtml(tnefText), isHtml: false };
|
||||
}
|
||||
// Embedded message/rfc822 unwrapped content
|
||||
if (embeddedEmailHtml) {
|
||||
@@ -2470,12 +2452,7 @@ export function EmailViewer({
|
||||
return { html: cleanHtml, isHtml: true };
|
||||
}
|
||||
if (embeddedEmailText) {
|
||||
const htmlFromText = embeddedEmailText
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/(https?:\/\/[^\s<]+)/g, '<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>');
|
||||
return { html: htmlFromText, isHtml: false };
|
||||
return { html: plainTextToSafeHtml(embeddedEmailText), isHtml: false };
|
||||
}
|
||||
return emailContent;
|
||||
}, [cidBlobUrls, emailContent, smimeDecryptedHtml, smimeDecryptedText, tnefHtml, tnefText, embeddedEmailHtml, embeddedEmailText]);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import DOMPurify from "dompurify";
|
||||
import { Email, ThreadGroup } from "@/lib/jmap/types";
|
||||
import { EMAIL_SANITIZE_CONFIG, collapseBlockedImageContainers } from "@/lib/email-sanitization";
|
||||
import { EMAIL_SANITIZE_CONFIG, collapseBlockedImageContainers, plainTextToSafeHtml } from "@/lib/email-sanitization";
|
||||
import { hasMeaningfulHtmlBody } from "@/lib/signature-utils";
|
||||
import { transformInlineStyles, transformColorForDarkMode, transformBgColorForDarkMode } from "@/lib/color-transform";
|
||||
import { useThemeStore } from "@/stores/theme-store";
|
||||
@@ -419,12 +419,7 @@ function EmailCard({
|
||||
// Plain text fallback
|
||||
if (email.textBody?.[0]?.partId && email.bodyValues[email.textBody[0].partId]) {
|
||||
const text = email.bodyValues[email.textBody[0].partId].value;
|
||||
const htmlEscaped = text
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/(https?:\/\/[^\s<]+)/g, '<a href="$1" target="_blank" rel="noopener noreferrer" class="text-primary hover:underline">$1</a>');
|
||||
return { html: htmlEscaped, isHtml: false };
|
||||
return { html: plainTextToSafeHtml(text, 'text-primary hover:underline'), isHtml: false };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user