Security-Hardening Runde 5: Hack-Das-Ding (DSGVO-GAU + Timing + XSS)
Live-Pentest gegen Dev-Server + 3 parallele Audit-Agents. 🚨 CRITICAL: /api/uploads/* war ohne Auth erreichbar - express.static('/api/uploads', ...) → jeder konnte mit ratbarer URL sensible PDFs (Kündigungsbestätigungen, Ausweise, Bankkarten, Vollmachten) ziehen. Live-verifiziert: 23-KB-PDF eines echten Kunden ohne Login geladen. - Fix: authenticate-Middleware vor static-Handler (req.query.token unterstützung war schon da, jetzt aktiv genutzt). - Frontend: utils/fileUrl.ts hängt JWT als ?token=... an. 24 direkte /api${...Path}-URLs in 5 Dateien per Skript migriert (CustomerDetail, ContractDetail, InvoicesSection, PdfTemplates, GDPRDashboard). 🚨 HIGH: Login-Timing User-Enumeration - bcrypt.compare wurde nur bei existierenden Usern ausgeführt → 110ms vs 10ms Differenz, Email-Enumeration trivial messbar. - Fix: Dummy-bcrypt-compare bei invalid user (Cost 12). Plus Lazy- Rehash bei erfolgreichem Login: alte Cost-10-Hashes (z.B. admin aus Installation) werden auf BCRYPT_COST upgraded, damit Dummy- und Echt-Hash-Cost zusammenpassen. - Live-verifiziert nach Admin-Rehash: 422ms (invalid) vs 423ms (valid) – Side-Channel dicht. 🚨 HIGH: XSS via Privacy-Policy/Imprint-HTML - 4 Frontend-Seiten renderten Backend-HTML ohne DOMPurify (PortalPrivacy, ConsentPage, PortalWebsitePrivacy, PortalImprint). Admin-eingegebene <script>-Tags wären bei jedem Portal-Kunden- Besuch ausgeführt worden – auch auf der öffentlichen Consent-Seite. - Fix: DOMPurify.sanitize mit strikter FORBID_TAGS/ATTR Config. 🛡 HIGH: IDOR-Härtung an Upload-/Document-Endpoints - canAccessContract jetzt in: uploadContractDocument, deleteContractDocument, handleContractDocumentUpload (Kündigungs- Letter+Confirmation), handleContractDocumentDelete, saveAttachmentAsContractDocument. - Defense-in-Depth: aktuell durch requirePermission abgesichert, schützt auch gegen künftige Staff-Scoping-Rollen. Offen für v1.1: - Per-File-Ownership-Check für /api/uploads (Kontroll-Lookup welche Ressource zur Datei gehört) - TipTap-Link-Tool javascript:-Protokoll blockieren - Prisma-Error-Messages in Admin-Endpoints generisch sanitisieren Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,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';
|
||||
|
||||
const typeLabels: Record<ContractType, string> = {
|
||||
ELECTRICITY: 'Strom',
|
||||
@@ -2034,7 +2035,7 @@ export default function ContractDetail() {
|
||||
{c.cancellationLetterPath ? (
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<a
|
||||
href={`/api${c.cancellationLetterPath}`}
|
||||
href={fileUrl(c.cancellationLetterPath)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:underline text-sm flex items-center gap-1"
|
||||
@@ -2043,7 +2044,7 @@ export default function ContractDetail() {
|
||||
Anzeigen
|
||||
</a>
|
||||
<a
|
||||
href={`/api${c.cancellationLetterPath}`}
|
||||
href={fileUrl(c.cancellationLetterPath)}
|
||||
download
|
||||
className="text-blue-600 hover:underline text-sm flex items-center gap-1"
|
||||
>
|
||||
@@ -2091,7 +2092,7 @@ export default function ContractDetail() {
|
||||
<>
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<a
|
||||
href={`/api${c.cancellationConfirmationPath}`}
|
||||
href={fileUrl(c.cancellationConfirmationPath)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:underline text-sm flex items-center gap-1"
|
||||
@@ -2100,7 +2101,7 @@ export default function ContractDetail() {
|
||||
Anzeigen
|
||||
</a>
|
||||
<a
|
||||
href={`/api${c.cancellationConfirmationPath}`}
|
||||
href={fileUrl(c.cancellationConfirmationPath)}
|
||||
download
|
||||
className="text-blue-600 hover:underline text-sm flex items-center gap-1"
|
||||
>
|
||||
@@ -2177,7 +2178,7 @@ export default function ContractDetail() {
|
||||
{c.cancellationLetterOptionsPath ? (
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<a
|
||||
href={`/api${c.cancellationLetterOptionsPath}`}
|
||||
href={fileUrl(c.cancellationLetterOptionsPath)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:underline text-sm flex items-center gap-1"
|
||||
@@ -2186,7 +2187,7 @@ export default function ContractDetail() {
|
||||
Anzeigen
|
||||
</a>
|
||||
<a
|
||||
href={`/api${c.cancellationLetterOptionsPath}`}
|
||||
href={fileUrl(c.cancellationLetterOptionsPath)}
|
||||
download
|
||||
className="text-blue-600 hover:underline text-sm flex items-center gap-1"
|
||||
>
|
||||
@@ -2234,7 +2235,7 @@ export default function ContractDetail() {
|
||||
<>
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<a
|
||||
href={`/api${c.cancellationConfirmationOptionsPath}`}
|
||||
href={fileUrl(c.cancellationConfirmationOptionsPath)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:underline text-sm flex items-center gap-1"
|
||||
@@ -2243,7 +2244,7 @@ export default function ContractDetail() {
|
||||
Anzeigen
|
||||
</a>
|
||||
<a
|
||||
href={`/api${c.cancellationConfirmationOptionsPath}`}
|
||||
href={fileUrl(c.cancellationConfirmationOptionsPath)}
|
||||
download
|
||||
className="text-blue-600 hover:underline text-sm flex items-center gap-1"
|
||||
>
|
||||
@@ -3310,7 +3311,7 @@ function ContractDocumentsSection({
|
||||
{doc.documentType}
|
||||
</span>
|
||||
<a
|
||||
href={`/api${doc.documentPath}`}
|
||||
href={fileUrl(doc.documentPath)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-blue-600 hover:underline"
|
||||
@@ -3327,7 +3328,7 @@ function ContractDocumentsSection({
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<a
|
||||
href={`/api${doc.documentPath}`}
|
||||
href={fileUrl(doc.documentPath)}
|
||||
download
|
||||
className="text-gray-400 hover:text-blue-600"
|
||||
title="Herunterladen"
|
||||
|
||||
Reference in New Issue
Block a user