fix(PROJ-81): BUG-81-3 - PDF-Vorschau via pdf.js statt sandboxed iframe

Chromium/Brave zeigte leeren PDF-Bereich (PDFium/MimeHandlerView
verweigert Rendering in sandbox="allow-scripts" ohne allow-same-origin,
Firefox war nicht betroffen). allow-same-origin nachzurüsten kam nicht
infrage: Blob-URLs erben den App-Origin, die Kombination hätte die
Sandbox faktisch aufgehoben (PROJ-61-Kontext).

Neu: PdfCanvasPreview.tsx rendert PDF via pdf.js selbst nach <canvas>,
kein iframe/eingebettetes Dokument mehr nötig. Parsing im Worker, kein
PDF-eigenes JavaScript, kein Text-/Annotationslayer im DOM. Seitenlimit
30, eigener Fehlerzweig für passwortgeschützte/beschädigte PDFs.

Neue Dependency pdfjs-dist@6.2.108 - Restrisiko: PDF-Parsing läuft
jetzt im App-Origin statt im Browser-Viewer-Prozess.

Noch nicht im Browser verifiziert, Status bleibt In Review.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WapWkrQusDuBMhaN8WyuXB
This commit is contained in:
sysops
2026-08-06 19:14:02 +02:00
co-authored by Claude Sonnet 5
parent ca693ded1b
commit 1ad1132006
5 changed files with 490 additions and 24 deletions
+35 -6
View File
@@ -77,7 +77,7 @@ Mail-Ansicht (/mail/[id])
### D) Abhängigkeiten (Pakete)
- PDF-Viewer-Bibliothek im Frontend (Anzeige im Dialog, ohne Download). Bilder benötigen keine zusätzliche Bibliothek (natives `<img>`).
- 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)
@@ -252,20 +252,49 @@ Kein Code-Fix. AC1 oben umformuliert: statt "Klick auf Anhang öffnet Vorschau"
**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)
- **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.
- **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.
### 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: 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.
**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_