npm run lint rief next lint auf, das es in Next.js 16 nicht mehr gibt — seit dem Next-16-Upgrade lief effektiv gar kein Lint mehr. Umgestellt auf eslint . mit Flat-Config (eslint.config.mjs statt .eslintrc.json). Der dadurch wieder sichtbare Lint-Lauf zeigte 30 Findings (25 Fehler, 5 Warnungen), alle gefixt: - 19x react-hooks/set-state-in-effect: Loading-States wo möglich als echte Ableitung statt eigenem Effect-State (use-mobile.tsx komplett auf useSyncExternalStore umgebaut), sonst async-Wrapper mit Cancel-Guard um bestehende Loader — Timing/Ladeanzeige unverändert. - react-hooks/refs (useSearch.ts): Ref-Schreibzugriff aus dem Render in einen Effect verschoben. - 4x no-html-link-for-pages: <a href> durch next/link ersetzt in admin/login, forgot-password, signup. - Rest (exhaustive-deps, no-img-element, unused disable) einzeln gefixt. - 4 bewusst belassene disable-Kommentare mit Begründung (shadcn/ui-Datei, QR-Code-data-URL, Full-Reload nach Auth laut Projektregel). eslint-Major-Upgrade auf 10 selbst bleibt blockiert: eslint-plugin-react/ jsx-a11y/import unterstützen ESLint 10 in ihrer aktuellen Latest-Version noch nicht (Crash beim Laden), siehe Feature-Spec PROJ-79. Verifiziert auf 132 (Build-Sandbox, kein Live-Deploy): npm ci/tsc/lint/ build grün, 8 Kern-Routen per Standalone-Server auf HTTP 200 geprüft. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019j28kGcaJAhBnrYX34hGdt
84 lines
3.4 KiB
Markdown
84 lines
3.4 KiB
Markdown
---
|
|
id: PROJ-76
|
|
title: Mail-HTML-Sanitizing schließt CSS-url()/link/srcset nicht ein (Tracking-Pixel-Umgehung)
|
|
status: Deployed
|
|
created: 2026-08-05
|
|
---
|
|
|
|
## Problem
|
|
|
|
`blockExternalSrcs()` (`src/app/mail/[id]/page.tsx`, Zeile ~76-83) filtert
|
|
per Regex ausschließlich `src=`-Attribute in `img|video|audio|source`-Tags.
|
|
Nicht abgedeckt:
|
|
|
|
- `<link rel="stylesheet" href=...>`
|
|
- `background=`-Attribut
|
|
- CSS `url()` in `<style>`-Blöcken oder `style=`-Attributen
|
|
- `srcset=`
|
|
|
|
Solche Remote-Referenzen laden auch im aktuellen "blockiert"-Zustand und
|
|
ermöglichen Read-Tracking (Öffnungs-Tracking-Pixel) trotz aktivierter
|
|
Blockierung — DSGVO-relevant, da archivierte E-Mails von Nutzern als
|
|
"sicher betrachtet" gelesen werden.
|
|
|
|
Regex-basiertes HTML-Filtern ist grundsätzlich umgehbar; eine robuste
|
|
Lösung braucht serverseitiges Sanitizing oder einen CSP-Header statt reiner
|
|
Client-Regex.
|
|
|
|
## Lösung (Vorschlag)
|
|
|
|
1. Kurzfristig: Regex in `blockExternalSrcs()` um `link[href]`,
|
|
`background=`, `srcset=` und CSS-`url()`-Vorkommen erweitern.
|
|
2. Mittelfristig: serverseitiges HTML-Sanitizing beim Rendern der Mail
|
|
(z.B. Allowlist-basierter Sanitizer statt Blocklist-Regex) statt
|
|
ausschließlich clientseitigem Regex-Blocking.
|
|
3. Alternative/Ergänzung: CSP-Header (`Content-Security-Policy` mit
|
|
`img-src 'none'` etc.) auf der Mail-Detail-Route, damit der Browser
|
|
selbst externe Loads blockiert statt sich auf Regex-Vorverarbeitung zu
|
|
verlassen.
|
|
|
|
Sollte mit dem `firewall-security`-Skill abgestimmt werden (CSP-Header-
|
|
Konfiguration liegt in dessen Zuständigkeit).
|
|
|
|
## Implementation Notes
|
|
|
|
Umgesetzt: Punkt 1 der Lösung (Regex-Erweiterung). Punkt 2 (serverseitiges
|
|
Sanitizing) und Punkt 3 (CSP-Header) bleiben offen.
|
|
|
|
`src/app/mail/[id]/page.tsx:80-165` — `blockExternalSrcs()` komplett ersetzt.
|
|
Statt zwei Einzel-Regexes auf `src=` jetzt ein zweistufiger Durchlauf:
|
|
|
|
1. `<style>…</style>`-Blöcke: externe `url(...)` → `url(about:blank)`
|
|
(`blockCssUrls()`).
|
|
2. Tag-Walker über alle Tags; `blockTagAttributes()` prüft `src`, `srcset`,
|
|
`href`, `background`, `style` (quoted/unquoted Werte):
|
|
- `src` → `data-src` nur bei `img|video|audio|source` (wie bisher)
|
|
- `srcset` → `data-srcset` bei `img|source`, wenn ein kommagetrennter
|
|
Kandidat extern ist
|
|
- `href` → `data-href` **nur** bei `<link>` (`<a href>` bleibt klickbar)
|
|
- `background` → `data-background` (Legacy-HTML-Mails)
|
|
- `style` → externe `url(...)` neutralisiert
|
|
|
|
Externerkennung via `EXTERNAL_URL_RE = /^\s*(?:https?:)?\/\//i` — deckt
|
|
zusätzlich protokollrelative URLs (`//host/px.gif`) ab, die die alte
|
|
`["']https?:`-Prüfung durchgelassen hätte. `data:`- und `cid:`-URIs werden
|
|
nicht angefasst, Inline-Bilder rendern weiter.
|
|
|
|
Manuell gegengeprüft (Node-Snippet, 14 Fälle): alle genannten Tracking-Vektoren
|
|
werden neutralisiert, `data:`/`cid:`/`<a href>`/Plaintext bleiben unverändert.
|
|
`npx tsc --noEmit` fehlerfrei.
|
|
|
|
Bekannte Restlücken (Blocklist-Ansatz bleibt umgehbar, daher Punkt 2/3):
|
|
`@import "http://…"` in CSS, `<iframe|embed>`-Quellen, Meta-Refresh.
|
|
|
|
Deployed auf 131 (Produktiv) am 2026-08-05.
|
|
|
|
## Acceptance Criteria
|
|
|
|
- [x] Test-Mail mit Tracking-Pixel via `<link>`, `background=`, CSS
|
|
`url()` und `srcset` lädt keine externe Ressource mehr in der
|
|
Mail-Detailansicht.
|
|
- [x] Bestehende, bereits blockierte Vektoren (`img|video|audio|source`
|
|
`src=`) weiterhin blockiert (Regressionstest).
|
|
- [ ] CSP-Header-Ansatz mit firewall-security-Skill abgestimmt.
|