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:
@@ -20,7 +20,7 @@ import CopyButton, { CopyableBlock } from '../../components/ui/CopyButton';
|
||||
import { formatDate } from '../../utils/dateFormat';
|
||||
import { useProviderSettings } from '../../hooks/useProviderSettings';
|
||||
import type { ContractType, ContractStatus, SimCard, MeterReading, ContractTask, ContractTaskSubtask, ContractMeter, ContractDocument } from '../../types';
|
||||
import { fileUrl } from '../../utils/fileUrl';
|
||||
import { fileUrl, viewUrl } from '../../utils/fileUrl';
|
||||
|
||||
const typeLabels: Record<ContractType, string> = {
|
||||
ELECTRICITY: 'Strom',
|
||||
@@ -2118,7 +2118,7 @@ export default function ContractDetail() {
|
||||
{c.cancellationLetterPath ? (
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<a
|
||||
href={fileUrl(c.cancellationLetterPath)}
|
||||
href={viewUrl(c.cancellationLetterPath)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:underline text-sm flex items-center gap-1"
|
||||
@@ -2175,7 +2175,7 @@ export default function ContractDetail() {
|
||||
<>
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<a
|
||||
href={fileUrl(c.cancellationConfirmationPath)}
|
||||
href={viewUrl(c.cancellationConfirmationPath)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:underline text-sm flex items-center gap-1"
|
||||
@@ -2261,7 +2261,7 @@ export default function ContractDetail() {
|
||||
{c.cancellationLetterOptionsPath ? (
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<a
|
||||
href={fileUrl(c.cancellationLetterOptionsPath)}
|
||||
href={viewUrl(c.cancellationLetterOptionsPath)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:underline text-sm flex items-center gap-1"
|
||||
@@ -2318,7 +2318,7 @@ export default function ContractDetail() {
|
||||
<>
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<a
|
||||
href={fileUrl(c.cancellationConfirmationOptionsPath)}
|
||||
href={viewUrl(c.cancellationConfirmationOptionsPath)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:underline text-sm flex items-center gap-1"
|
||||
@@ -3484,7 +3484,7 @@ function ContractDocumentsSection({
|
||||
{doc.documentType}
|
||||
</span>
|
||||
<a
|
||||
href={fileUrl(doc.documentPath)}
|
||||
href={viewUrl(doc.documentPath)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-blue-600 hover:underline"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user