Files
opencrm/frontend/src/utils/fileUrl.ts
T
duffyduck 0bd2f9be7e fix: "Anzeigen"-Buttons öffnen Datei wieder im Browser-Tab
Folge-Symptom des Pen-30.13-Fixes: alle file-downloads liefen mit
Content-Disposition: attachment – das ist gegen Stored-XSS richtig,
hat aber die "Anzeigen"-Buttons (Bankkarten / Ausweise /
Verträge / etc.) kaputtgemacht, weil der Browser jetzt
herunterlud statt im Tab zu öffnen.

Magic-Byte-basierter Whitelist-Pfad eingebaut: optional ?disposition=
inline am Download-Endpoint, ABER nur wenn die ersten Bytes der
Datei das Magic eines safe Typs zeigen (PDF, PNG, JPEG, GIF, WebP).
Bei Mismatch fällt's auf attachment zurück – Stored-XSS bleibt
weiterhin unmöglich, falls jemand HTML als .pdf hochlädt.

Frontend: neuer viewUrl(path)-Alias = fileUrl(path, {inline: true}).
Alle Stellen mit `<a href={fileUrl(...)} target="_blank">` oder
`window.open(fileUrl(...), '_blank')` (13 Stellen über CustomerDetail,
ContractDetail, PdfTemplates, GDPRDashboard, InvoicesSection)
nutzen jetzt viewUrl. Download-Stellen bleiben fileUrl
(= attachment, byte-genaues File-Save).

Live-verifiziert auf dev:
- ohne Param: attachment (default, Stored-XSS-Schutz)
- ?disposition=inline + echte PDF: inline + application/pdf
- ?disposition=inline + HTML als .pdf: attachment (Magic-Mismatch
  → Browser lädt herunter statt zu rendern)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 09:19:04 +02:00

40 lines
1.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Baut eine Download-URL für ein im Backend gespeichertes Upload-File.
*
* Geht über `GET /api/files/download?path=...` der Backend-Controller
* macht einen Per-File-Ownership-Check (Pfad → Resource → canAccessCustomer
* / canAccessContract). Damit kann auch ein eingeloggter User keine
* fremden Dateien abrufen, selbst wenn er den Pfad kennen würde.
*
* <a href> und window.open senden keinen Authorization-Header, daher
* Token als Query-Parameter (auth-Middleware akzeptiert `?token=<jwt>`).
*
* Trade-off: Tokens in URLs können in Logs/Referrer landen. Eine
* sauberere Lösung mit kurzlebigen Download-Tokens (signierte URLs)
* wäre v1.1-Item.
*/
import { getAccessToken } from '../services/api';
/**
* Kurzform für Inline-Vorschau: identisch zu `fileUrl(path, { inline: true })`.
* Verwenden für „Anzeigen"-Links / target="_blank"-Vorschauen. Default-
* `fileUrl(path)` bleibt für Downloads (Content-Disposition: attachment).
*/
export function viewUrl(path: string | null | undefined): string {
return fileUrl(path, { inline: true });
}
export function fileUrl(path: string | null | undefined, opts?: { inline?: boolean }): string {
if (!path) return '';
const token = getAccessToken();
const normalizedPath = path.startsWith('/') ? path : '/' + path;
// `?disposition=inline` schaltet die Anzeige im Browser-Tab ein,
// der Backend-Controller bleibt aber nur dann inline, wenn die
// Datei tatsächlich ein safe Type (PDF/PNG/JPEG/GIF/WebP) ist
// sonst fällt's auf attachment zurück. Default attachment.
const dispParam = opts?.inline ? '&disposition=inline' : '';
const base = `/api/files/download?path=${encodeURIComponent(normalizedPath)}${dispParam}`;
if (!token) return base;
return `${base}&token=${encodeURIComponent(token)}`;
}