mirror of
https://github.com/root-fr/jmap-webmail.git
synced 2026-09-26 08:01:20 +00:00
fix(security): prevent global CSS injection from email style tags
Emails containing <style> tags were injecting global CSS rules that affected the entire application UI. For example, button:hover rules from emails would override all button hover states app-wide. Changes: - Block <style> tags in email sanitization config (security fix) - Keep inline style attributes for element-specific formatting - Add explicit bg-transparent to banner buttons (defense in depth) - Add CSS isolation to banner container - Fix theme dependency in email content memoization This prevents malicious or poorly-formatted emails from breaking the UI or being used for phishing via CSS injection attacks.
This commit is contained in:
@@ -524,7 +524,7 @@ export function EmailViewer({
|
||||
html: '<p style="color: var(--color-muted-foreground);">No content available</p>',
|
||||
isHtml: false
|
||||
};
|
||||
}, [email, allowExternalContent, hasBlockedContent, externalContentPolicy, isSenderTrusted]);
|
||||
}, [email, allowExternalContent, hasBlockedContent, externalContentPolicy, isSenderTrusted, theme]);
|
||||
|
||||
// Detect List-Unsubscribe header for newsletter banners
|
||||
const listHeaders = useMemo(() => {
|
||||
@@ -1290,9 +1290,9 @@ export function EmailViewer({
|
||||
{/* Unified Notification Banner - External Content + Unsubscribe */}
|
||||
{((hasBlockedContent && !allowExternalContent && externalContentPolicy !== 'allow') ||
|
||||
(shouldShowUnsubBanner && listHeaders?.listUnsubscribe)) && (
|
||||
<div className="border-b border-border bg-muted/30">
|
||||
<div className="border-b border-border bg-muted/30 isolate">
|
||||
<div className="max-w-4xl mx-auto px-6 py-1.5">
|
||||
<div className="flex flex-col md:flex-row md:items-center md:justify-center gap-3">
|
||||
<div className="flex flex-col md:flex-row md:items-center md:justify-center gap-3 isolate">
|
||||
{/* External Content Controls */}
|
||||
{hasBlockedContent && !allowExternalContent && externalContentPolicy !== 'allow' && (
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
@@ -1300,7 +1300,7 @@ export function EmailViewer({
|
||||
{externalContentPolicy === 'ask' && (
|
||||
<button
|
||||
onClick={() => setAllowExternalContent(true)}
|
||||
className="flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors min-h-[44px] md:min-h-0"
|
||||
className="flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground bg-transparent hover:bg-transparent transition-colors min-h-[44px] md:min-h-0"
|
||||
>
|
||||
<Image className="w-3.5 h-3.5" />
|
||||
{t('load_external_content')}
|
||||
@@ -1316,7 +1316,7 @@ export function EmailViewer({
|
||||
setAllowExternalContent(true);
|
||||
}
|
||||
}}
|
||||
className="flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors min-h-[44px] md:min-h-0"
|
||||
className="flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground bg-transparent hover:bg-transparent transition-colors min-h-[44px] md:min-h-0"
|
||||
>
|
||||
{t('trust_sender')}
|
||||
</button>
|
||||
|
||||
@@ -92,7 +92,7 @@ export function UnsubscribeBanner({
|
||||
</span>
|
||||
<button
|
||||
onClick={onDismiss}
|
||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors min-h-[44px] md:min-h-0"
|
||||
className="text-sm text-muted-foreground hover:text-foreground bg-transparent hover:bg-transparent transition-colors min-h-[44px] md:min-h-0"
|
||||
>
|
||||
{t('email_viewer.unsubscribe_banner.dismiss')}
|
||||
</button>
|
||||
@@ -110,14 +110,14 @@ export function UnsubscribeBanner({
|
||||
<button
|
||||
onClick={handleUnsubscribe}
|
||||
disabled={processing}
|
||||
className="flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors disabled:opacity-50 disabled:cursor-not-allowed min-h-[44px] md:min-h-0"
|
||||
className="flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground bg-transparent hover:bg-transparent transition-colors disabled:opacity-50 disabled:cursor-not-allowed min-h-[44px] md:min-h-0"
|
||||
>
|
||||
{processing && <Loader2 className="w-3.5 h-3.5 animate-spin" />}
|
||||
{t('email_viewer.unsubscribe_banner.confirm_button')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowConfirm(false)}
|
||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors min-h-[44px] md:min-h-0"
|
||||
className="text-sm text-muted-foreground hover:text-foreground bg-transparent hover:bg-transparent transition-colors min-h-[44px] md:min-h-0"
|
||||
>
|
||||
{t('email_viewer.unsubscribe_banner.cancel')}
|
||||
</button>
|
||||
@@ -125,7 +125,7 @@ export function UnsubscribeBanner({
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setShowConfirm(true)}
|
||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors min-h-[44px] md:min-h-0"
|
||||
className="text-sm text-muted-foreground hover:text-foreground bg-transparent hover:bg-transparent transition-colors min-h-[44px] md:min-h-0"
|
||||
>
|
||||
{t('email_viewer.unsubscribe_banner.button')}
|
||||
</button>
|
||||
|
||||
@@ -3,16 +3,18 @@ import DOMPurify from 'dompurify';
|
||||
/**
|
||||
* Unified DOMPurify configuration for email content
|
||||
* Blocks all script execution vectors while preserving formatting
|
||||
* NOTE: <style> tags are forbidden to prevent global CSS injection
|
||||
* Inline style attributes are still allowed for element-specific styling
|
||||
*/
|
||||
export const EMAIL_SANITIZE_CONFIG = {
|
||||
ADD_TAGS: ['style'],
|
||||
ADD_TAGS: [],
|
||||
ADD_ATTR: ['target', 'style', 'class', 'width', 'height', 'align', 'valign', 'bgcolor', 'color'],
|
||||
ALLOW_DATA_ATTR: false,
|
||||
FORCE_BODY: true,
|
||||
FORBID_TAGS: [
|
||||
'script', 'iframe', 'object', 'embed', 'form',
|
||||
'input', 'button', 'meta', 'link', 'base',
|
||||
'svg', 'math'
|
||||
'svg', 'math', 'style'
|
||||
],
|
||||
FORBID_ATTR: [
|
||||
'onerror', 'onload', 'onclick', 'onmouseover',
|
||||
|
||||
Reference in New Issue
Block a user