From bd427a944ea5dd6a946bd3aae9f3dc50ed7620e9 Mon Sep 17 00:00:00 2001 From: sysops Date: Thu, 6 Aug 2026 18:28:12 +0200 Subject: [PATCH] =?UTF-8?q?fix(PROJ-81):=20BUG-81-5/-6=20-=20AC1-Wortlaut?= =?UTF-8?q?=20angeglichen,=20Content-Type-Fallback=20f=C3=BCr=20endungslos?= =?UTF-8?q?e=20Anh=C3=A4nge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AC1-Text auf tatsächlichen Vorschau-Button-Ansatz umformuliert - resolveMimeType(): Server-content_type nur als Hinweis bei fehlender Dateiendung, kann vorhandene Endung nicht überstimmen; Rendering bleibt strikt auf clientseitige MIME-Whitelist beschränkt (Security-Modell unverändert, kein SVG/HTML) Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01WapWkrQusDuBMhaN8WyuXB --- features/PROJ-81-anhang-online-vorschau.md | 23 +++++++- .../mail/AttachmentPreviewDialog.tsx | 56 ++++++++++++++++--- 2 files changed, 69 insertions(+), 10 deletions(-) diff --git a/features/PROJ-81-anhang-online-vorschau.md b/features/PROJ-81-anhang-online-vorschau.md index 897482b..bf40c09 100644 --- a/features/PROJ-81-anhang-online-vorschau.md +++ b/features/PROJ-81-anhang-online-vorschau.md @@ -17,7 +17,7 @@ Mail-Anhänge lassen sich in `/mail/[id]` aktuell nur herunterladen (`downloadMa - Als Admin/Auditor will ich, dass Vorschau nur für Anhänge funktioniert, auf die der jeweilige Tenant/User laut bestehender Zugriffsregeln berechtigt ist (keine neue Sicherheitslücke). ## Acceptance Criteria -- [ ] Klick auf Anhang öffnet Vorschau als Modal/Dialog über der aktuellen Mail-Ansicht +- [ ] Jede Anhang-Zeile hat einen eigenen "Vorschau"-Button, der die Vorschau als Modal/Dialog über der aktuellen Mail-Ansicht öffnet (bewusst ein separater Button statt Klick auf die ganze Zeile, damit "Vorschau" und "Herunterladen" eindeutig unterscheidbar bleiben — siehe BUG-81-5) - [ ] PDF-Anhänge werden inline gerendert (Browser-natives PDF-Rendering oder eingebetteter Viewer) - [ ] Bild-Anhänge (jpg, png, gif, webp) werden als Bildvorschau angezeigt - [ ] Nicht unterstützte Dateitypen (inkl. Office-Dokumente wie docx/xlsx) zeigen Hinweis "Keine Vorschau verfügbar" + Download-Button bleibt erhalten @@ -240,13 +240,32 @@ Der Grund für den Guard steht als Kommentar direkt am Effekt, damit er bei kün ### BUG-81-4 (Low) — Sicherheitskommentar widersprach dem Code — **fixed** Der Datei-Header behauptete "sandboxed iframe ohne allow-same-origin/allow-scripts", tatsächlich ist `sandbox="allow-scripts"` gesetzt. Header korrigiert: `allow-scripts` **ja**, `allow-same-origin` **nein** (undurchsichtiger Origin, kein Cookie-/DOM-Zugriff), inkl. Begründung warum `allow-scripts` nötig ist. Ein falscher Security-Kommentar ist gefährlich, weil künftige Reviews darauf vertrauen — daher trotz Low-Severity sofort mitgenommen. +### BUG-81-5 (Low) — AC1-Wortlaut vs. Umsetzung — **fixed (Spec-Text)** +Kein Code-Fix. AC1 oben umformuliert: statt "Klick auf Anhang öffnet Vorschau" beschreibt es jetzt den tatsächlich umgesetzten separaten "Vorschau"-Button je Anhang-Zeile, inkl. Begründung (Vorschau und Herunterladen bleiben eindeutig unterscheidbar). Spec und Code stimmen damit überein. + +### BUG-81-6 (Low) — Anhänge ohne Dateiendung bekamen nie Vorschau — **fixed** +`AttachmentPreviewDialog.tsx`: Die Typ-Erkennung läuft jetzt über `resolveMimeType()`: +1. Dateiendung `pdf` bzw. bekannte Bild-Endung → gewinnt weiterhin, unverändert. +2. **Nur bei komplett fehlender Endung** (`scan`, `Rechnung` — typisch für Fax-/Scanner-Gateways) wird der Server-`content_type` als Hinweis herangezogen, über die neue Konstante `ALLOWED_MIME_TYPES` (Parameter wie `; charset=…` werden abgeschnitten, Vergleich case-insensitiv). +3. Unbekannte Endung → weiterhin "keine Vorschau"; der Server-Wert kann eine vorhandene Endung **nicht** überstimmen. + +**Das Security-Modell bleibt unverändert:** der Server-Wert entscheidet ausschließlich, *welcher* Eintrag aus der festen Whitelist gewählt wird. Gerendert wird nach wie vor nur mit dem clientseitig erzwungenen MIME-Typ aus dieser Liste. Ein Wert außerhalb der Whitelist — insbesondere `image/svg+xml` (PROJ-61-Vektor) oder `text/html` — führt zu "keine Vorschau", nicht zum Rendern. SVG ist in `ALLOWED_MIME_TYPES` bewusst nicht enthalten. +Ein Angreifer gewinnt dadurch nichts: er könnte über den Content-Type höchstens erreichen, dass seine endungslose Datei als PDF oder Bild *interpretiert* wird — genau die beiden Pfade, die ohnehin als sicher ausgelegt sind (erzwungener MIME-Typ, PDF im Sandbox-iframe ohne `allow-same-origin`). + ### Offen, wie abgestimmt (kein Code-Fix in diesem Durchgang) - **BUG-81-3** — PDF-Sandbox in Firefox/Safari unbelegt: nach Deploy im Browser verifizieren. Fällt das Rendering aus, braucht es einen sichtbaren Hinweis statt eines leeren weißen Rahmens. -- **BUG-81-5** (AC1-Wortlaut "Klick auf Anhang" vs. Vorschau-Button) und **BUG-81-6** (Anhänge ohne Dateiendung bekommen keinen Vorschau-Button) — nicht angefasst. - **Audit-Logging für Anhang-Abrufe** (Bestandsfund aus dem Security-Audit): bewusst **nicht** hier mitgefixt, bekommt ein eigenes Ticket. - Die drei Backend-Fragen sind vom QA-Lauf beantwortet (nosniff nur über nginx, `Content-Disposition: attachment` gesetzt, **keine** serverseitige Content-Type-Whitelist). Für die Vorschau unkritisch, weil der Dialog den Server-Content-Type ignoriert; die empfohlene Handler-seitige Härtung ist Backend-Scope. Nach den Fixes: `npx tsc --noEmit` und `npm run build` fehlerfrei. +### Stand nach zweitem Fix-Durchgang +Alle im QA-Bericht gemeldeten Bugs sind erledigt: BUG-81-1 (High/Blocker), BUG-81-2 (Medium), BUG-81-4 (Low), BUG-81-5 (Low, Spec-Text), BUG-81-6 (Low). Offen bleibt allein **BUG-81-3** — die PDF-Darstellung im Sandbox-iframe ist für Firefox und Safari unbelegt und lässt sich nur im echten Browser gegen eine laufende Instanz prüfen. + +**Deployment-Empfehlung: deploybar auf 132.** Kein offener Blocker im Code. Der Status bleibt bewusst auf **In Review** und geht erst auf Deployed, wenn nach dem Deploy verifiziert ist: +- BUG-81-3: PDF-Vorschau in Chrome, Firefox, Safari, Edge. Rendert ein Browser nichts, braucht es einen sichtbaren Hinweis statt eines leeren weißen Rahmens. +- Gegenprobe zu BUG-81-1: fehlgeschlagene Vorschau (z.B. 404) erzeugt genau **einen** Request, keine Schleife — im Netzwerk-Tab prüfen. +- Gegenprobe zu BUG-81-6: endungsloser PDF-Anhang bekommt einen Vorschau-Button und rendert. + ## Deployment _To be added by /deploy_ diff --git a/src/components/mail/AttachmentPreviewDialog.tsx b/src/components/mail/AttachmentPreviewDialog.tsx index 13ee91a..ec6a6a9 100644 --- a/src/components/mail/AttachmentPreviewDialog.tsx +++ b/src/components/mail/AttachmentPreviewDialog.tsx @@ -25,6 +25,8 @@ import { * Dokument im App-Origin ausgefuehrt werden. * * - SVG ist absichtlich NICHT in der Whitelist (SVG kann Skripte enthalten). + * - Der Server-Content-Type wird nur bei Anhaengen OHNE Dateiendung als Hinweis + * zur Auswahl aus derselben Whitelist genutzt (BUG-81-6), nie zum Rendern. * - PDFs laufen in einem sandboxed iframe MIT allow-scripts, aber ohne * allow-same-origin: der Frame hat dadurch einen eigenen, undurchsichtigen * Origin und keinen Zugriff auf Cookies/DOM der App. allow-scripts ist noetig, @@ -56,14 +58,54 @@ function extensionOf(filename: string): string { return filename.slice(idx + 1).toLowerCase(); } +/** Erlaubte MIME-Typen. Nur was hier steht, kann ueberhaupt gerendert werden. */ +const ALLOWED_MIME_TYPES: Record = { + "application/pdf": "application/pdf", + "image/jpeg": "image/jpeg", + "image/jpg": "image/jpeg", + "image/png": "image/png", + "image/gif": "image/gif", + "image/webp": "image/webp", + "image/bmp": "image/bmp", +}; + /** - * Entscheidet allein anhand der Dateiendung, ob und wie vorgeschaut wird. - * Bewusst nicht anhand des Server-Content-Type (siehe Security-Hinweis oben). + * BUG-81-6: Anhaenge ohne Dateiendung (z.B. "scan", "Rechnung" von Fax-/ + * Scanner-Gateways) bekamen nie eine Vorschau. Nur in diesem Fall wird der + * Server-`content_type` als *Hinweis* herangezogen. + * + * Das Security-Modell bleibt unveraendert: der Server-Wert entscheidet nur, + * WELCHER Eintrag aus der festen Whitelist gewaehlt wird. Gerendert wird + * ausschliesslich mit dem clientseitig erzwungenen MIME-Typ aus dieser Liste; + * ein Wert ausserhalb der Whitelist (z.B. image/svg+xml oder text/html) fuehrt + * zu "keine Vorschau", nicht zum Rendern. + * + * Bei vorhandener Dateiendung gewinnt weiterhin die Endung — ein Server-Wert + * kann eine bekannte Endung also nicht ueberstimmen. + */ +function hintedMimeType(attachment: MailAttachment): string | null { + const raw = (attachment.content_type || "").split(";")[0].trim().toLowerCase(); + return ALLOWED_MIME_TYPES[raw] ?? null; +} + +/** Ermittelt den erzwungenen MIME-Typ, oder null wenn keine Vorschau moeglich. */ +function resolveMimeType(attachment: MailAttachment): string | null { + const ext = extensionOf(attachment.filename); + if (ext === PDF_EXT) return "application/pdf"; + if (ext in IMAGE_TYPES) return IMAGE_TYPES[ext]; + if (ext === "") return hintedMimeType(attachment); + return null; +} + +/** + * Entscheidet, ob und wie vorgeschaut wird — primaer ueber die Dateiendung, + * bei fehlender Endung hilfsweise ueber den Server-Content-Type (siehe + * hintedMimeType). In beiden Faellen nur aus fester Whitelist. */ export function previewKindFor(attachment: MailAttachment): PreviewKind { - const ext = extensionOf(attachment.filename); - if (ext === PDF_EXT) return "pdf"; - if (ext in IMAGE_TYPES) return "image"; + const mime = resolveMimeType(attachment); + if (mime === "application/pdf") return "pdf"; + if (mime?.startsWith("image/")) return "image"; return "unsupported"; } @@ -72,9 +114,7 @@ export function canPreview(attachment: MailAttachment): boolean { } function forcedMimeType(attachment: MailAttachment): string { - const ext = extensionOf(attachment.filename); - if (ext === PDF_EXT) return "application/pdf"; - return IMAGE_TYPES[ext] ?? "application/octet-stream"; + return resolveMimeType(attachment) ?? "application/octet-stream"; } function formatBytes(n: number): string {