fix(PROJ-81): BUG-81-5/-6 - AC1-Wortlaut angeglichen, Content-Type-Fallback für endungslose Anhänge

- 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WapWkrQusDuBMhaN8WyuXB
This commit is contained in:
sysops
2026-08-06 18:28:12 +02:00
co-authored by Claude Sonnet 5
parent 0a02bd7112
commit bd427a944e
2 changed files with 69 additions and 10 deletions
@@ -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<string, string> = {
"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 {