EmailDetail: Links immer im neuen Tab öffnen
Nach DOMPurify-Sanitize alle <a>-Elemente auf target="_blank" + rel="noopener noreferrer" setzen. Letzteres verhindert window.opener-Tab-Hijacking. Sanitize + DOM-Walk in useMemo, läuft nur bei Wechsel der Email neu.
This commit is contained in:
@@ -97,6 +97,12 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
|
|||||||
|
|
||||||
## ✅ Erledigt
|
## ✅ Erledigt
|
||||||
|
|
||||||
|
- [x] **🆕 Email-Links öffnen im neuen Tab**
|
||||||
|
- In `EmailDetail` nach der DOMPurify-Sanitize jedes `<a>`-Element
|
||||||
|
auf `target="_blank"` + `rel="noopener noreferrer"` gesetzt. Letzteres
|
||||||
|
verhindert window.opener-Tab-Hijacking. Sanitize + DOM-Walk laufen
|
||||||
|
in einem `useMemo`, das nur bei Wechsel der Email neu rechnet.
|
||||||
|
|
||||||
- [x] **🐞 assertSafePdf: jspdf-PDFs mit JPEGs fälschlich als „JavaScript" blockiert**
|
- [x] **🐞 assertSafePdf: jspdf-PDFs mit JPEGs fälschlich als „JavaScript" blockiert**
|
||||||
- Stage-Bug: User lädt Ausweis als „JPGs → PDF" hoch → 415 mit
|
- Stage-Bug: User lädt Ausweis als „JPGs → PDF" hoch → 415 mit
|
||||||
Meldung „PDF enthält JavaScript-Action". Backend hat den jspdf-
|
Meldung „PDF enthält JavaScript-Action". Backend hat den jspdf-
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
import { Reply, Forward, RotateCcw, Star, Paperclip, Link2, X, Download, ExternalLink, Trash2, Undo2, Save, FileDown } from 'lucide-react';
|
import { Reply, Forward, RotateCcw, Star, Paperclip, Link2, X, Download, ExternalLink, Trash2, Undo2, Save, FileDown } from 'lucide-react';
|
||||||
import DOMPurify from 'dompurify';
|
import DOMPurify from 'dompurify';
|
||||||
import { CachedEmail, cachedEmailApi } from '../../services/api';
|
import { CachedEmail, cachedEmailApi } from '../../services/api';
|
||||||
@@ -52,6 +52,24 @@ export default function EmailDetail({
|
|||||||
setLocalStarred(email.isStarred);
|
setLocalStarred(email.isStarred);
|
||||||
}, [email.id, email.isStarred]);
|
}, [email.id, email.isStarred]);
|
||||||
|
|
||||||
|
// Email-Body sanitizen + alle <a>-Links auf neuen Tab umstellen.
|
||||||
|
// rel="noopener noreferrer" verhindert window.opener-Tab-Hijacking.
|
||||||
|
const safeHtmlBody = useMemo(() => {
|
||||||
|
if (!email.htmlBody) return '';
|
||||||
|
const sanitized = DOMPurify.sanitize(email.htmlBody, {
|
||||||
|
FORBID_TAGS: ['script', 'style', 'iframe', 'object', 'embed', 'form'],
|
||||||
|
FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover', 'onfocus'],
|
||||||
|
ADD_ATTR: ['target'],
|
||||||
|
});
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.innerHTML = sanitized;
|
||||||
|
wrapper.querySelectorAll('a').forEach((a) => {
|
||||||
|
a.setAttribute('target', '_blank');
|
||||||
|
a.setAttribute('rel', 'noopener noreferrer');
|
||||||
|
});
|
||||||
|
return wrapper.innerHTML;
|
||||||
|
}, [email.htmlBody]);
|
||||||
|
|
||||||
const toggleStarMutation = useMutation({
|
const toggleStarMutation = useMutation({
|
||||||
mutationFn: () => cachedEmailApi.toggleStar(email.id),
|
mutationFn: () => cachedEmailApi.toggleStar(email.id),
|
||||||
onMutate: () => {
|
onMutate: () => {
|
||||||
@@ -411,16 +429,7 @@ export default function EmailDetail({
|
|||||||
{showHtml && email.htmlBody ? (
|
{showHtml && email.htmlBody ? (
|
||||||
<div
|
<div
|
||||||
className="prose prose-sm max-w-none"
|
className="prose prose-sm max-w-none"
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{ __html: safeHtmlBody }}
|
||||||
__html: DOMPurify.sanitize(email.htmlBody, {
|
|
||||||
// Scripte, Inline-Handler, Form-Elemente, externe Referenzen verbieten.
|
|
||||||
// Bilder + Links mit target=_blank bleiben zugelassen.
|
|
||||||
FORBID_TAGS: ['script', 'style', 'iframe', 'object', 'embed', 'form'],
|
|
||||||
FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover', 'onfocus'],
|
|
||||||
// Links in neuen Tabs öffnen (verhindert window.opener-Angriffe)
|
|
||||||
ADD_ATTR: ['target'],
|
|
||||||
}),
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<pre className="whitespace-pre-wrap text-sm text-gray-700 font-sans">
|
<pre className="whitespace-pre-wrap text-sm text-gray-700 font-sans">
|
||||||
|
|||||||
Reference in New Issue
Block a user