Files
archivmail/features/PROJ-81-anhang-online-vorschau.md
sysopsandClaude Sonnet 5 2aad899dfc docs(PROJ-81): Pflegehinweis für pdfjs-dist in Spec ergänzt
Seit dem BUG-81-3-Fix läuft PDF-Parsing über pdfjs-dist im App-Origin
statt in einer Browser-Sandbox. Sicherheitsniveau hängt damit an der
Code-Qualität der Dependency - Update-Pflicht bei Security-Releases
dokumentiert, nicht erst beim nächsten großen Dependency-Sweep.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WapWkrQusDuBMhaN8WyuXB
2026-08-06 19:19:43 +02:00

27 KiB

PROJ-81: Anhang-Online-Vorschau (PDF, Bilder)

Status: In Review

Created: 2026-08-06 Last Updated: 2026-08-06

Kontext

Mail-Anhänge lassen sich in /mail/[id] aktuell nur herunterladen (downloadMailAttachment), keine Online-Ansicht im Browser. PROJ-81 ergänzt eine Vorschau direkt im Frontend, damit User Anhänge nicht erst lokal öffnen müssen.

Dependencies

  • Baut auf PROJ-7 (E-Mail-Ansicht) auf — Anhang-Liste existiert bereits in /mail/[id]

User Stories

  • Als User will ich einen PDF-Anhang direkt im Browser ansehen können, ohne ihn herunterzuladen.
  • Als User will ich Bild-Anhänge (jpg/png/gif) als Vorschau sehen, bevor ich sie herunterlade.
  • Als User will ich die Vorschau in einem Dialog/Overlay öffnen, ohne die Mail-Ansicht zu verlassen.
  • 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

  • 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
  • Download-Option bleibt im Dialog zusätzlich verfügbar (Vorschau ersetzt Download nicht)
  • Vorschau respektiert bestehende Tenant-Isolation/Zugriffsrechte (kein direkter öffentlicher Link auf Rohdatei)
  • Große Anhänge (Performance-Grenze definieren, z.B. > 20 MB) zeigen Warnhinweis statt automatischem Laden

Edge Cases

  • Sehr große PDF/Bild-Dateien (>20 MB) → Warnhinweis statt automatischer Vorschau, User muss Laden explizit bestätigen
  • Passwortgeschützte/verschlüsselte PDFs → Vorschau schlägt fehl, Fehlermeldung + Download-Fallback
  • Beschädigte/korrupte Anhänge → Fehlermeldung statt Absturz des Dialogs
  • Sehr viele Anhänge in einer Mail → Performance beim Öffnen mehrerer Vorschauen nacheinander
  • Anhang wurde per DSGVO-Löschersuchen (PROJ-50) bereits entfernt → Vorschau zeigt "nicht mehr verfügbar" statt Fehler

Technical Requirements (optional)

  • Performance: PDF/Bild-Vorschau < 2s Ladezeit bei normaler Dateigröße
  • Security: Vorschau-Endpunkt muss denselben Auth-/Tenant-Check wie bestehender Download-Endpunkt durchlaufen
  • Browser Support: Chrome, Firefox, Safari, Edge
  • Pflege: pdfjs-dist regelmäßig aktuell halten. Seit dem Fix zu BUG-81-3 läuft das PDF-Parsing über die Bibliothek pdfjs-dist im App-Origin (Web Worker, kein iframe/Browser-Sandbox mehr, siehe Implementation Notes unten). Das Sicherheitsniveau der PDF-Vorschau hängt damit direkt an der Code-Qualität dieser Dependency — ein Parser-Bug dort landet im App-Origin, nicht in einer isolierten Browser-Komponente. pdfjs-dist ist aktiv gepflegt (Basis des Firefox-eigenen PDF-Viewers) und regelmäßiges CVE-Ziel. Bei Major-/Security-Updates zeitnah aktualisieren, nicht auf den nächsten großen Dependency-Sweep (vgl. PROJ-79) warten.

Tech Design (Solution Architect)

A) Komponentenstruktur (visuell)

Mail-Ansicht (/mail/[id])
+-- Anhang-Liste (bestehend)
|   +-- Anhang-Zeile
|       +-- "Vorschau"-Button (NEU)
|       +-- "Herunterladen"-Button (bestehend, bleibt)
+-- Vorschau-Dialog (NEU, Overlay über aktueller Seite)
    +-- Titelzeile (Dateiname, Schließen-Button)
    +-- Inhalt (je nach Typ):
    |   +-- PDF-Ansicht (eingebetteter Viewer)
    |   +-- Bild-Ansicht (Vollbild-Bild, zoombar)
    |   +-- "Keine Vorschau verfügbar"-Hinweis (Fallback, u.a. für Office-Dokumente)
    +-- Ladezustand (Spinner bei großer Datei)
    +-- Warnhinweis bei sehr großen Dateien ("trotzdem laden?")
    +-- "Herunterladen"-Button (bleibt zusätzlich verfügbar)

B) Datenfluss (Klartext, kein neues Datenmodell)

  • Vorschau nutzt denselben bestehenden Anhang-Endpunkt wie der heutige Download (/api/mails/{id}/attachments/{index}), der bereits Tenant-/Zugriffsprüfung durchläuft — kein neuer öffentlicher Link, keine neue Sicherheitsfläche.
  • PDF und Bilder: Browser stellt Datei direkt dar, keine Serververarbeitung nötig.
  • Kein neuer Datenbank-Eintrag nötig — Vorschau ist ein reiner Anzeige-Vorgang, keine dauerhaft gespeicherte Information.
  • Kein Server-seitiger Konvertierungsschritt (Office-Dokumente bewusst außerhalb des Scopes, siehe unten).

C) Tech-Entscheidungen (Begründung)

  • Vorschau als Modal statt neue Seite/Route: User bleibt im Kontext der Mail, kein Verlust der aktuellen Ansicht/Scroll-Position (laut Klärung des Nutzers gewünscht).
  • Bestehenden Anhang-Endpunkt wiederverwenden statt neuen zu bauen: Zugriffsrechte (Tenant-Isolation, Auth) sind dort bereits korrekt implementiert — Wiederverwendung vermeidet doppelte Sicherheitslogik und Inkonsistenzrisiko.
  • Office-Dokumente (docx/xlsx) bewusst außerhalb des Scopes: Nutzer hat Scope auf PDF+Bilder reduziert. Damit entfällt der größte Aufwandstreiber (Server-seitige Konvertierung, neue Systemabhängigkeit). Reines Frontend-Feature ohne neue Backend-Logik.
  • Größenlimit + Warnhinweis statt hartem Blocken: Verhindert, dass ein einzelner sehr großer Anhang den Browser blockiert, lässt dem User aber die Wahl, ihn trotzdem zu laden.

D) Abhängigkeiten (Pakete)

  • PDF-Viewer-Bibliothek im Frontend (Anzeige im Dialog, ohne Download): pdfjs-dist@6.2.108, ergänzt im dritten Fix-Durchgang (BUG-81-3). Der zunächst genutzte browsereigene Viewer im iframe war nicht browserübergreifend nutzbar. Bilder benötigen keine zusätzliche Bibliothek (natives <img>).
  • Keine neuen Server-/Systemabhängigkeiten.

Implementation Notes (Frontend, 2026-08-06)

Neue Komponente: src/components/mail/AttachmentPreviewDialog.tsx Angebunden in src/app/mail/[id]/page.tsx (AttachmentRow): pro Anhang erscheint ein "Vorschau"-Button, aber nur wenn der Typ vorschaufähig ist; "Herunterladen" bleibt unverändert daneben und zusätzlich im Dialog.

Keine neue API-Funktion nötig — der Dialog nutzt das bestehende downloadMailAttachment(mailId, index) und damit denselben Endpunkt inkl. bestehender Auth-/Tenant-Prüfung. src/lib/api/index.ts musste nicht ergänzt werden.

Security-Umsetzung (PROJ-61-Kontext)

Abweichung vom Tech Design in der Umsetzung, bewusst strenger:

  • Der Anhang wird per authentifiziertem fetch als Blob geholt, nicht per direkter Navigation auf die Anhang-URL.
  • Der vom Server gelieferte Content-Type wird nicht zum Rendern verwendet. Der Blob wird clientseitig mit einem erzwungenen MIME-Typ aus einer festen Whitelist neu verpackt. Eine als rechnung.pdf getarnte HTML-Datei kann dadurch nicht als aktives Dokument im App-Origin ausgeführt werden.
  • Whitelist: pdfapplication/pdf; jpg/jpeg/png/gif/webp/bmp → jeweiliger Bildtyp. SVG ist bewusst ausgeschlossen (kann Skripte enthalten — genau der PROJ-61-Vektor).
  • PDF rendert in <iframe sandbox="allow-scripts"> ohne allow-same-origin: eigener undurchsichtiger Origin, kein Zugriff auf Cookies/DOM der App. allow-scripts ist nötig, weil die browsereigenen PDF-Viewer sonst nicht rendern.
  • Blob-URL lebt nur im Tab und wird beim Schließen des Dialogs bzw. beim Unmount per URL.revokeObjectURL freigegeben — kein persistenter öffentlicher Link.

Damit ist die Vorschau unabhängig davon sicher, welche Header das Backend setzt.

Weitere Details

  • Größenlimit: PREVIEW_SIZE_WARN_BYTES = 20 MB. Darüber wird nicht automatisch geladen, sondern "Trotzdem laden" angeboten.
  • Zustände umgesetzt: Laden (Spinner), Fehler (Alert + Download-Fallback), "Keine Vorschau verfügbar" (u.a. docx/xlsx), Bild-onError fängt korrupte Dateien ab.
  • Bild-/PDF-Fläche auf neutral hellem Grund, damit transparente PNGs auch im Dark Mode sichtbar sind (Konsistenz mit PROJ-80).
  • Responsive: Dialog w-[95vw] max-w-4xl, Buttons stapeln unter sm.

Build: npx tsc --noEmit und npm run build fehlerfrei.

Offen / abzustimmen mit Backend Developer

Nicht blockierend für die Frontend-Funktion, aber zur Härtung des Endpunkts /api/mails/{id}/attachments/{index} bestätigen zu lassen:

  1. Wird X-Content-Type-Options: nosniff gesetzt?
  2. Wird Content-Disposition: attachment gesetzt (verhindert Inline-Rendering bei direktem Aufruf der URL im Browser)?
  3. Wird der gespeicherte Content-Type unverändert aus der Mail übernommen, oder serverseitig auf eine Whitelist normalisiert? Der Dialog ist unabhängig von den Antworten sicher, weil er den Server-Content-Type ignoriert — die Fragen betreffen den Fall, dass jemand die Anhang-URL direkt aufruft.

QA Test Results

Getestet: 2026-08-06 | Methode: Statische Code-Verifikation + Backend-Endpunkt-Live-Test gegen 192.168.1.132

Testbarkeits-Einschränkung (wichtig)

Der Frontend-Code ist uncommitted und nicht deployed (git status: ?? src/components/mail/AttachmentPreviewDialog.tsx, M src/app/mail/[id]/page.tsx). Auf 132 läuft der alte Stand. Ein visuelles/funktionales Durchklicken der Vorschau im Browser war daher NICHT möglich. Alle UI-Kriterien sind per Code-Review bewertet, nicht per Live-Test.

Acceptance Criteria

# Kriterium Ergebnis Anmerkung
1 Klick auf Anhang öffnet Vorschau als Modal PASS (mit Abweichung) Umgesetzt als separater "Vorschau"-Button, nicht als Klick auf die Zeile → BUG-81-5
2 PDF inline gerendert UNVERIFIZIERT Code vorhanden (<iframe sandbox="allow-scripts">), Rendering in FF/Safari fraglich → BUG-81-3
3 Bilder (jpg/png/gif/webp) als Vorschau PASS Whitelist inkl. bmp, <img> mit onError-Fallback
4 Nicht unterstützte Typen zeigen "Keine Vorschau verfügbar" FAIL Hinweis ist unerreichbar, Button wird gar nicht gerendert → BUG-81-2
5 Download bleibt im Dialog verfügbar PASS Download-Button im Dialog-Footer + unverändert in der Zeile
6 Tenant-Isolation / kein öffentlicher Link PASS Kein neuer Endpunkt; Blob-URL tab-lokal + revokeObjectURL
7 Große Anhänge (>20 MB) zeigen Warnhinweis PASS PREVIEW_SIZE_WARN_BYTES = 20 MB, "Trotzdem laden"-Bestätigung

Summe: 4 PASS, 1 FAIL, 1 PASS-mit-Abweichung, 1 unverifiziert

Edge Cases

Edge Case Ergebnis
>20 MB → Bestätigung statt Autoload PASS
Passwortgeschütztes PDF FAIL (indirekt) — Fehlerpfad löst BUG-81-1 aus
Beschädigter Anhang TEILWEISE — img onError greift; Fetch-Fehler löst BUG-81-1 aus
Viele Anhänge nacheinander PASS — Dialog wird pro Zeile nur bei previewOpen gemountet, Blob wird beim Unmount freigegeben
Anhang per DSGVO-Löschung entfernt (404) FAIL — löst BUG-81-1 (Endlosschleife) aus

Bugs

BUG-81-1 — Endlos-Retry-Schleife bei jedem Vorschau-Fehler — Severity: High — Priorität 1

AttachmentPreviewDialog.tsx:143-149

useEffect(() => {
  if (!open) return;
  if (kind === "unsupported") return;
  if (isLarge && !confirmedLarge) return;
  if (urlRef.current || loading) return;
  void load();
}, [open, kind, isLarge, confirmedLarge, loading, load]);

Der Effekt hat keinen error-Guard. Ablauf bei einem fehlgeschlagenen Laden:

  1. load() schlägt fehl → setError(...), setLoading(false)
  2. loading wechselt true → false → Effekt feuert erneut (steht in den Deps)
  3. urlRef.current ist null, loading ist false → alle Guards passieren → load() erneut
  4. → Endlosschleife

Reproduktion: Mail mit Anhang öffnen, Vorschau eines Anhangs starten, dessen Abruf fehlschlägt (Backend antwortet 403/404/500 — z.B. per DSGVO-Löschersuchen entfernter Anhang, oder Netzwerk offline schalten). Auswirkung: Browser-Tab blockiert/heizt, und der Client feuert unbegrenzt Requests gegen /api/mails/{id}/attachments/{index} — selbstverursachter API-Dauerbeschuss pro geöffnetem Dialog. Trifft genau die dokumentierten Edge Cases (korrupter Anhang, gelöschter Anhang, passwortgeschütztes PDF). Blocker für Produktion.

BUG-81-2 — AC4 nicht erfüllt: "Keine Vorschau verfügbar" ist toter Code — Severity: Medium — Priorität 2

src/app/mail/[id]/page.tsx:366 rendert den Vorschau-Button nur bei previewable === true (canPreview()). Für docx/xlsx/zip gibt es damit keinen Weg, den Dialog zu öffnen — der Zweig kind === "unsupported" in AttachmentPreviewDialog.tsx:175-183 ist unerreichbar. AC4 verlangt explizit, dass nicht unterstützte Typen den Hinweis zeigen. Entweder Button immer rendern (und Hinweis anzeigen) oder AC anpassen — die Spec muss mit der Implementierung übereinstimmen.

BUG-81-3 — PDF-Rendering im Sandbox-iframe browserübergreifend unverifiziert — Severity: Medium — Priorität 3

AttachmentPreviewDialog.tsx:241-248: <iframe src={blobUrl} sandbox="allow-scripts"> ohne allow-same-origin. Firefox' pdf.js und Safari rendern PDFs in sandboxed iframes ohne allow-same-origin erfahrungsgemäß nicht zuverlässig; zusätzlich weigert sich Safari häufig, blob:-PDFs in iframes darzustellen. Das Technical Requirement "Browser Support: Chrome, Firefox, Safari, Edge" ist damit nicht belegt. Erforderlich: manuelle Verifikation in allen vier Browsern nach dem Deploy. Falls FF/Safari leer bleiben, braucht es einen sichtbaren Fallback ("PDF kann in diesem Browser nicht angezeigt werden") statt eines weißen Rahmens.

BUG-81-4 — Sicherheitskommentar widerspricht dem Code — Severity: Low — Priorität 4

AttachmentPreviewDialog.tsx:28 behauptet: "PDFs laufen in einem sandboxed iframe ohne allow-same-origin/allow-scripts". Tatsächlich setzt Zeile 246 sandbox="allow-scripts". Der Inline-Kommentar bei Zeile 236-239 beschreibt es korrekt. Ein falscher Security-Kommentar im Datei-Header ist gefährlich, weil künftige Reviews darauf vertrauen.

BUG-81-5 — AC1-Wortlaut vs. Umsetzung — Severity: Low — Priorität 5

AC1: "Klick auf Anhang öffnet Vorschau". Umgesetzt: zusätzlicher "Vorschau"-Button. Funktional gleichwertig und arguably besser (Download bleibt eindeutig), aber Spec und Code sollten angeglichen werden.

BUG-81-6 — Anhänge ohne Dateiendung bekommen nie Vorschau — Severity: Low — Priorität 6

previewKindFor() entscheidet ausschließlich über die Dateiendung. Ein echtes PDF, das als scan oder Rechnung (ohne Endung) in der Mail steckt — bei Fax-/Scanner-Gateways nicht selten — bekommt keinen Vorschau-Button. Vorschlag: bei fehlender Endung zusätzlich den Server-content_type als Hinweis nutzen (weiterhin nicht zum Rendern, sondern nur zur Auswahl des erzwungenen MIME-Typs aus der Whitelist).


Security Audit (Red Team)

Ergebnis: keine neue Angriffsfläche durch PROJ-81. Die Umsetzung ist an den kritischen Stellen strenger als das Tech Design und korrekt.

Prüfpunkt Ergebnis
Neuer Endpunkt? Nein — /api/mails/{id}/attachments/{index} wiederverwendet, keine neue Route in internal/api/server.go
Auth ohne Cookie PASS — live gegen 132: GET /api/mails/abc/attachments/0401
Tenant-Isolation (PROJ-61-Klasse) PASS — search_handlers.go:406-413 vergleicht sess.TenantID gegen GetTenantForMail(); zusätzlich auditorIsGlobal()-Zweig (PROJ-55) und RoleUser-Ownership-Check
RBAC PASS — requireMailAccess() (server.go:423-438) blockt superadmin/domain_admin vom Mailinhalt (SEC-29)
Stored XSS via getarntem Anhang PASS — Server-Content-Type wird ignoriert, Blob mit erzwungenem MIME aus fester Whitelist neu verpackt; SVG bewusst ausgeschlossen; PDF-iframe ohne allow-same-origin (undurchsichtiger Origin, kein Cookie-/DOM-Zugriff)
Persistenter öffentlicher Link PASS — URL.revokeObjectURL bei Close und Unmount (urlRef-Muster deckt auch Unmount während des Ladens ab)

Antworten auf die offenen Backend-Fragen aus den Implementation Notes:

  1. X-Content-Type-Options: nosniffja, aber nur über nginx, nicht im Go-Handler. Live auf 132 verifiziert, greift auch auf /api/-Antworten. Empfehlung (Härtung, nicht blockierend): zusätzlich im Handler setzen — der Go-Dienst auf :8080 liefert den Header sonst ohne Proxy nicht, und andere Handler wie tenant_logo_handlers.go:44 setzen ihn bereits selbst.
  2. Content-Disposition: attachmentja, search_handlers.go:447. Direkter Aufruf der URL im Browser rendert also nicht inline.
  3. Content-Type-Normalisierung — nein. search_handlers.go:446 setzt w.Header().Set("Content-Type", a.ContentType) unverändert aus der Mail, ohne Whitelist. Risiko durch Punkt 1+2 gemindert, aber eine serverseitige Whitelist wäre die saubere Defense-in-Depth.

Bestandsfunde (nicht durch PROJ-81 verursacht, aber durch das Feature relevanter):

  • Anhang-Abrufe werden nicht ins Audit-Log geschrieben. handleGetAttachment (search_handlers.go:378-450) enthält keinen Audit-Aufruf. Da die Vorschau denselben Endpunkt nutzt, greifen User künftig deutlich häufiger auf Anhangsinhalte zu, ohne dass es nachvollziehbar ist — GoBD-/DSGVO-relevant. Severity: Medium, eigenes Ticket empfohlen.
  • Existenz-Orakel: unbekannte Mail-ID → 404, fremde Mail-ID → 403. Erlaubt Enumeration der Existenz fremder Mail-IDs. Severity: Low, Bestandsverhalten (auch in handleGetRaw).

Regression

  • npx tsc --noEmit fehlerfrei (Exit 0).
  • Keine Backend-Änderung → keine Regression an PROJ-55 (Auditor-Scoping), PROJ-50 (DSGVO-Löschung), SEC-29 zu erwarten.
  • src/app/mail/[id]/page.tsx (PROJ-7) angefasst: AttachmentRow umgebaut, Download-Pfad unverändert; HTML-Mail-Container zusätzlich bg-white (PROJ-80-Änderung, siehe dort).
  • Regression an PROJ-76 (Mail-HTML-Sanitizing) und PROJ-61 (Logo-XSS) nicht berührt.

Produktionsreife: NEIN

Blocker: BUG-81-1 (High). Zusätzlich sollte BUG-81-2 (AC4 nicht erfüllt) vor dem Deploy geklärt werden, und BUG-81-3 verlangt eine echte Browser-Verifikation nach dem Deploy auf 132.


Bugfixes nach QA (Frontend, 2026-08-06)

BUG-81-1 (High, Blocker) — Endlos-Retry-Schleife — fixed

AttachmentPreviewDialog.tsx: if (error) return; als Guard ergänzt, error in die Dep-Liste des Lade-Effekts aufgenommen. Nach einem Fehlschlag wird nicht mehr automatisch neu geladen — die Schleife (loading true→false → Effekt feuert → load() → Fehler → …) ist damit unterbrochen und der Dauerbeschuss von /api/mails/{id}/attachments/{index} beendet. Der User kommt weiterhin ans Ziel: Download-Button bleibt im Dialog, und beim Schließen wird error zurückgesetzt, ein erneutes Öffnen startet also einen sauberen neuen Versuch. Der Grund für den Guard steht als Kommentar direkt am Effekt, damit er bei künftigen Refactorings nicht wieder wegfällt.

BUG-81-2 (Medium) — AC4-Fallback war toter Code — fixed

src/app/mail/[id]/page.tsx: Der "Vorschau"-Button wird jetzt für alle Anhänge gerendert, nicht mehr nur bei canPreview(). docx/xlsx/zip öffnen den Dialog und erhalten dort den Hinweis "Keine Vorschau verfügbar" plus Download-Button — AC4 ist damit erfüllt und der Zweig erreichbar. canPreview wird in page.tsx nicht mehr importiert, bleibt aber als Export erhalten (intern weiter über previewKindFor genutzt).

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).

BUG-81-3 (Medium) — PDF-Vorschau blank in Chromium/Brave — fixed

Befund aus dem Live-Test auf 132: Firefox rendert die PDF-Vorschau einwandfrei, Chromium/Brave zeigt eine leere weiße Fläche — kein Fehler, kein Download-Prompt. Ursache bestätigt: Chromiums PDFium läuft als MimeHandlerView-Extension und benötigt dafür Same-Origin-Zugriff; in einem iframe mit sandbox="allow-scripts" ohne allow-same-origin verweigert er sich still. Firefox' pdf.js-basierter Viewer kommt damit klar. Kein Spec-Verstoß, sondern Chromium-spezifisches Verhalten.

Bewertung Option 1 (allow-same-origin ergänzen) — verworfen

Die entscheidende Frage war, ob eine Blob-URL einen eigenen opaken Origin hat. Hat sie nicht. URL.createObjectURL() erzeugt eine URL der Form blob:https://host/uuid; der Origin des daraus geladenen Dokuments ist per Spezifikation der Origin des erzeugenden Kontexts, also unser App-Origin. Opak wird ein Frame nur durch die Sandbox selbst (allow-same-origin weggelassen) — nicht durch das Blob.

Daraus folgt: mit sandbox="allow-scripts allow-same-origin" auf same-origin-Inhalt wäre der Frame vollwertig App-Origin. Diese Kombination hebt die Sandbox praktisch auf, weil das eingebettete Dokument über parent.document an das Sandbox-Attribut selbst herankommt und sich per Re-Navigation entsandboxen kann; die HTML-Spezifikation warnt genau vor dieser Kombination. Der Frame hätte damit Zugriff auf DOM, nicht-httpOnly-Cookies, localStorage und credential-behaftete API-Aufrufe.

Man könnte einwenden, dass ein getarntes HTML-Dokument wegen des erzwungenen Blob-MIME-Typs (application/pdf) ohnehin nie als HTML geparst wird — das stimmt. Aber damit hinge die gesamte Absicherung an genau einer Kontrolle. Für ein Archiv mit fremdem, unkontrolliertem Mail-Inhalt und der PROJ-61-Vorgeschichte ist das die falsche Richtung.

Umgesetzt: Option 2 (pdf.js gebündelt)

Neue Komponente src/components/mail/PdfCanvasPreview.tsx, neue Dependency pdfjs-dist@6.2.108.

  • PDF wird von pdf.js selbst nach <canvas> gerendert — kein iframe, kein eingebettetes Dokument, damit entfällt die Sandbox-Frage vollständig statt sie aufzuweichen.
  • Verhalten ist in Chromium und Firefox identisch, weil nicht mehr vom Viewer des Browsers abhängig.
  • Parsing läuft im Worker; das Worker-Bundle wird vom Build als statisches Asset emittiert (verifiziert: .next/static/media/pdf.worker.min.*.mjs).
  • PDF-eigenes JavaScript wird nicht ausgeführt: das Scripting-Sandbox-Bundle (pdf.sandbox) wird nicht geladen. isEvalSupported gibt es in pdfjs-dist v6 nicht mehr, weil eval() dort bereits vollständig entfernt wurde.
  • Es wird ausschließlich in Canvas gezeichnet, kein Text-/Annotationslayer — es gelangt also kein HTML aus dem PDF ins DOM.
  • Seitenlimit MAX_RENDERED_PAGES = 30 mit Hinweis, damit ein PDF mit sehr vielen Seiten den Tab nicht blockiert.
  • Eigener Fehlerzweig für passwortgeschützte/beschädigte PDFs (Edge Case aus der Spec) mit Download-Hinweis; doc.destroy() beim Unmount.

Im Dialog hält jetzt pdfBlob das PDF, Bilder laufen unverändert über die Blob-URL (Bildpfad nicht angefasst, er funktioniert live). Der Guard gegen die BUG-81-1-Schleife nutzt jetzt loadedRef statt urlRef, damit er beide Pfade abdeckt.

Restrisiko / ehrlich: die Verlagerung des PDF-Parsings in unseren Origin heißt, dass eine Schwachstelle in pdf.js im App-Origin landet statt im Viewer-Prozess des Browsers. pdf.js ist genau für feindliche PDFs ausgelegt, parst im Worker und ohne eval, und die Alternative (Option 1) hätte den App-Origin ohnehin direkt preisgegeben. Die Dependency muss aber mitgepflegt werden — pdf.js ist ein regelmäßiges CVE-Ziel.

Offen, wie abgestimmt (kein Code-Fix in diesem Durchgang)

  • 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 drittem Fix-Durchgang

Alle im QA-Bericht gemeldeten Bugs sind erledigt: BUG-81-1 (High/Blocker), BUG-81-2 (Medium), BUG-81-3 (Medium), BUG-81-4, BUG-81-5, BUG-81-6 (Low).

Deployment-Empfehlung: erneut auf 132 deployen und nachtesten. Der Status bleibt auf In Review, weil der PDF-Pfad komplett ausgetauscht wurde und der Fix bisher nur durch Build und statische Analyse belegt ist, nicht im Browser. Nach dem Deploy zu verifizieren:

  • PDF-Vorschau in Chromium/Brave — das war das ursprüngliche Symptom, hier muss jetzt gerendert werden.
  • PDF-Vorschau in Firefox — Regressionsprobe, dort funktionierte es vorher schon.
  • Mehrseitiges PDF: Seiten scrollbar, ab 30 Seiten erscheint der Kürzungshinweis.
  • Passwortgeschütztes/beschädigtes PDF: sichtbarer Fehlertext statt leerer Fläche.
  • Bild-Vorschau (JPG) unverändert funktionsfähig — der Pfad wurde bewusst nicht angefasst.
  • 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.
  • Ladezeit gegen das Technical Requirement (< 2 s bei normaler Dateigröße) gegenprüfen — pdf.js rendert langsamer als der native Viewer.

Deployment

To be added by /deploy