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
parent 6a670df1c4
commit 0bd2f9be7e
7 changed files with 78 additions and 28 deletions
@@ -21,7 +21,7 @@ import { formatDate } from '../../utils/dateFormat';
import { getContractTypeInfo } from '../../utils/contractInfo';
import { useProviderSettings } from '../../hooks/useProviderSettings';
import type { Address, BankCard, IdentityDocument, Meter, Customer, CustomerRepresentative, CustomerSummary, CustomerConsent, ConsentType, ConsentStatus, RepresentativeAuthorization } from '../../types';
import { fileUrl } from '../../utils/fileUrl';
import { fileUrl, viewUrl } from '../../utils/fileUrl';
export default function CustomerDetail({ portalCustomerId }: { portalCustomerId?: number } = {}) {
const { id } = useParams();
@@ -577,7 +577,7 @@ function BusinessDataCard({
{customer.businessRegistrationPath ? (
<div className="flex items-center gap-2 flex-wrap">
<a
href={fileUrl(customer.businessRegistrationPath)}
href={viewUrl(customer.businessRegistrationPath)}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline text-sm flex items-center gap-1"
@@ -628,7 +628,7 @@ function BusinessDataCard({
{customer.commercialRegisterPath ? (
<div className="flex items-center gap-2 flex-wrap">
<a
href={fileUrl(customer.commercialRegisterPath)}
href={viewUrl(customer.commercialRegisterPath)}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline text-sm flex items-center gap-1"
@@ -948,7 +948,7 @@ function BankCardsTab({
{card.documentPath ? (
<div className="flex items-center gap-2 flex-wrap">
<a
href={fileUrl(card.documentPath)}
href={viewUrl(card.documentPath)}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline text-sm flex items-center gap-1"
@@ -1184,7 +1184,7 @@ function DocumentsTab({
{doc.documentPath ? (
<div className="flex items-center gap-2 flex-wrap">
<a
href={fileUrl(doc.documentPath)}
href={viewUrl(doc.documentPath)}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline text-sm flex items-center gap-1"
@@ -4123,7 +4123,7 @@ function ConsentTab({
{customer.privacyPolicyPath ? (
<div className="flex items-center gap-3 flex-wrap">
<a
href={fileUrl(customer.privacyPolicyPath)}
href={viewUrl(customer.privacyPolicyPath)}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline text-sm flex items-center gap-1"
@@ -4441,7 +4441,7 @@ function AuthorizationsSection({ customerId, customerEmail }: { customerId: numb
{auth.documentPath ? (
<>
<a
href={fileUrl(auth.documentPath)}
href={viewUrl(auth.documentPath)}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline text-xs flex items-center gap-1"