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>
This commit is contained in:
2026-05-30 09:19:04 +02:00
co-authored by Claude Opus 4.7
parent 6a670df1c4
commit 0bd2f9be7e
7 changed files with 78 additions and 28 deletions
@@ -7,7 +7,7 @@ import Card from '../../components/ui/Card';
import Button from '../../components/ui/Button';
import Select from '../../components/ui/Select';
import { ArrowLeft, FileText, Users, CheckCircle, Clock, XCircle, AlertTriangle, Download, X, ChevronRight } from 'lucide-react';
import { fileUrl } from '../../utils/fileUrl';
import { viewUrl } from '../../utils/fileUrl';
import { useAuth } from '../../context/AuthContext';
const STATUS_OPTIONS = [
@@ -364,7 +364,7 @@ export default function GDPRDashboard() {
<Button
variant="ghost"
size="sm"
onClick={() => window.open(fileUrl(`/uploads/${request.proofDocument}`), '_blank')}
onClick={() => window.open(viewUrl(`/uploads/${request.proofDocument}`), '_blank')}
title="Löschnachweis anzeigen"
>
<FileText className="w-4 h-4 text-blue-500" />
+2 -2
View File
@@ -9,7 +9,7 @@ import Input from '../../components/ui/Input';
import Badge from '../../components/ui/Badge';
import Modal from '../../components/ui/Modal';
import { ArrowLeft, Plus, Edit, Trash2, FileText, Upload, Link2, Eye, Play } from 'lucide-react';
import { fileUrl } from '../../utils/fileUrl';
import { viewUrl } from '../../utils/fileUrl';
export default function PdfTemplates() {
const navigate = useNavigate();
@@ -96,7 +96,7 @@ export default function PdfTemplates() {
<Button variant="ghost" size="sm" onClick={() => setTestTemplate(t)} title="Testvorschau mit Vertragsdaten">
<Play className="w-4 h-4 text-green-500" />
</Button>
<a href={fileUrl(t.templatePath)} target="_blank" rel="noopener noreferrer">
<a href={viewUrl(t.templatePath)} target="_blank" rel="noopener noreferrer">
<Button variant="ghost" size="sm" title="Leere Vorlage anzeigen">
<Eye className="w-4 h-4" />
</Button>