From 25681075b47d569915bc78d6179465a8d1504364 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Tue, 2 Jun 2026 14:20:13 +0200 Subject: [PATCH] Pentest 24.6 INFO + 26.7 LOW: PENDING-Status sperren + documentPath-Validator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 24.6 (Portal kann Consent auf PENDING zurücksetzen): - gdpr.controller updateCustomerConsent prüft jetzt explizit, dass der Portal-User nur GRANTED oder WITHDRAWN setzen kann. PENDING ist nur der initiale System-Status; ein Reset darauf hätte die DSGVO-Auswertung verfälscht. 26.7 (documentPath ohne Validierung): - Neuer Helper isValidDocumentPath + assertValidDocumentPath in utils/sanitize: nur /?uploads/, keine "..", keine javascript:/data:/vbscript:, kein HTML. - consent.service.updateConsent ruft den Assert auf – Defense-in- Depth gegen zukünftige Caller, die documentPath aus User-Input durchreichen könnten. - authorization.service.grantAuthorization analog. - Cleanup-Skript (prisma/cleanup-xss-and-mass-assignment) entfernt seine lokale Kopie der Path-Validierung und nutzt den shared Helper – Single Source of Truth. 27.1 (Altdaten in Staging-DB): - Cleanup-Skript läuft sowieso bei jedem Container-Start. Nina- Records mit "../../../etc/passwd" werden beim nächsten Restart genullt (oder verschwinden mit dem VM-Snapshot-Wechsel). Live-Test isValidDocumentPath: 13/13 OK – legitime Pfade durch, Traversal/JS-URI/HTML blockiert. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../prisma/cleanup-xss-and-mass-assignment.ts | 18 +++++----------- backend/src/controllers/gdpr.controller.ts | 12 +++++++++++ backend/src/services/authorization.service.ts | 3 +++ backend/src/services/consent.service.ts | 7 +++++++ backend/src/utils/sanitize.ts | 21 +++++++++++++++++++ 5 files changed, 48 insertions(+), 13 deletions(-) diff --git a/backend/prisma/cleanup-xss-and-mass-assignment.ts b/backend/prisma/cleanup-xss-and-mass-assignment.ts index d369285b..955c5ed7 100644 --- a/backend/prisma/cleanup-xss-and-mass-assignment.ts +++ b/backend/prisma/cleanup-xss-and-mass-assignment.ts @@ -9,7 +9,7 @@ * mehrfach aufrufbar. */ import prisma from '../src/lib/prisma.js'; -import { stripHtml } from '../src/utils/sanitize.js'; +import { stripHtml, isValidDocumentPath as isValidDocumentPathShared } from '../src/utils/sanitize.js'; import { ALLOWED_SETTING_KEYS } from '../src/services/appSetting.service.js'; const CUSTOMER_STRING_FIELDS = [ @@ -93,18 +93,10 @@ const ALLOWED_CONSENT_SOURCES = new Set([ ]); // Legitimer documentPath: relativer Pfad unter uploads/, keine ".."-Segmente. -// Schreibend werden Pfade ausschließlich vom multer-Upload erzeugt -// (server-kontrollierter Dateiname), bestehende Pentest-Hinterlassenschaften -// wie "../../../etc/passwd" oder "javascript:alert(1)" müssen aus der DB -// raus (Pentest 2026-05-20 LOW 27.1). -function isValidDocumentPath(v: string | null | undefined): boolean { - if (!v) return true; // null/leer ist OK - if (v.includes('..')) return false; - if (/(?:javascript|data|vbscript)\s*:/i.test(v)) return false; - if (/<[a-z!\/]/i.test(v)) return false; // HTML im Pfad - // erlaubt: "uploads/...", "/uploads/..."; keine Kontrollzeichen - return /^\/?uploads\/[A-Za-z0-9._\-\/]+$/.test(v); -} +// Pentest 27.1 (2026-05-20). Helper jetzt zentral in utils/sanitize – wir +// re-exportieren hier nur, damit der bestehende Code keinen Schaden nimmt +// und wir nur EINE Quelle der Wahrheit pflegen müssen. +const isValidDocumentPath = isValidDocumentPathShared; async function cleanupConsents() { // version + documentPath: HTML strippen (waren ohne Validierung). diff --git a/backend/src/controllers/gdpr.controller.ts b/backend/src/controllers/gdpr.controller.ts index 3846760e..98de691c 100644 --- a/backend/src/controllers/gdpr.controller.ts +++ b/backend/src/controllers/gdpr.controller.ts @@ -309,6 +309,18 @@ export async function updateCustomerConsent(req: AuthRequest, res: Response) { return res.status(400).json({ success: false, error: 'Ungültiger Consent-Typ' }); } + // Pentest 24.6 (INFO, 2026-06-02): Portal-User durfte `PENDING` + // mitschicken und damit den Consent-Status auf den initialen System- + // Status zurücksetzen. PENDING ist nur intern (Default beim + // Customer-Anlegen); Portal darf nur GRANTED oder WITHDRAWN setzen. + // Verfälschte sonst die DSGVO-Auswertung. + if (status !== 'GRANTED' && status !== 'WITHDRAWN') { + return res.status(400).json({ + success: false, + error: 'Portal-Einwilligungen dürfen nur auf GRANTED oder WITHDRAWN gesetzt werden.', + }); + } + const consentLabels: Record = { DATA_PROCESSING: 'Datenverarbeitung', MARKETING_EMAIL: 'E-Mail-Marketing', diff --git a/backend/src/services/authorization.service.ts b/backend/src/services/authorization.service.ts index 3b901018..41dcf1f2 100644 --- a/backend/src/services/authorization.service.ts +++ b/backend/src/services/authorization.service.ts @@ -1,6 +1,7 @@ import prisma from '../lib/prisma.js'; import fs from 'fs'; import path from 'path'; +import { assertValidDocumentPath } from '../utils/sanitize.js'; /** * Vollmachten für einen Kunden abrufen (wer darf diesen Kunden einsehen?) @@ -53,6 +54,8 @@ export async function grantAuthorization( representativeId: number, data: { source?: string; documentPath?: string; notes?: string } ) { + // Pentest 26.7 (Defense-in-Depth): documentPath nur als /uploads/. + assertValidDocumentPath(data.documentPath, 'documentPath'); return prisma.representativeAuthorization.upsert({ where: { customerId_representativeId: { customerId, representativeId }, diff --git a/backend/src/services/consent.service.ts b/backend/src/services/consent.service.ts index 4c27c5ab..e72abbc2 100644 --- a/backend/src/services/consent.service.ts +++ b/backend/src/services/consent.service.ts @@ -2,6 +2,7 @@ import { ConsentType, ConsentStatus } from '@prisma/client'; import prisma from '../lib/prisma.js'; import fs from 'fs'; import path from 'path'; +import { assertValidDocumentPath } from '../utils/sanitize.js'; // Whitelist legitimer Werte für CustomerConsent.source. Schema-Kommentar: // "portal", "telefon", "papier", "email". Public-Link-Flow nutzt @@ -80,6 +81,12 @@ export async function updateConsent( throw new Error('Kunde nicht gefunden'); } + // Pentest 26.7: documentPath darf nur ein gültiger /uploads/-Pfad + // sein. Aktuell hat KEIN Endpoint diesen Wert aus User-Input gemappt + // (Portal: nicht aus Body, Admin-Auth-Upload: server-generated). Diese + // Service-Side-Validation ist Defense-in-Depth gegen zukünftige Caller. + assertValidDocumentPath(data.documentPath, 'documentPath'); + const now = new Date(); const updateData = { status: data.status, diff --git a/backend/src/utils/sanitize.ts b/backend/src/utils/sanitize.ts index 768608a7..2f4288a2 100644 --- a/backend/src/utils/sanitize.ts +++ b/backend/src/utils/sanitize.ts @@ -225,6 +225,27 @@ export function validateContractDocumentType(raw: unknown): string { return canonical; } +// Pentest 26.7 LOW (defense-in-depth, 2026-06-02): documentPath wird +// (außer beim Upload-Endpoint) NIE direkt aus User-Input übernommen. +// Falls doch jemand auf die Idee kommt, das Feld irgendwo zu mappen, +// fangen wir hier Path-Traversal / Javascript-URIs / HTML ab. +// Spiegelt isValidDocumentPath aus prisma/cleanup-xss-and-mass-assignment.ts +// 1:1 – Single Source of Truth für Lese- UND Schreibpfad. +export function isValidDocumentPath(v: string | null | undefined): boolean { + if (!v) return true; // null/leer ist OK – Feld bleibt einfach unbesetzt + if (typeof v !== 'string') return false; + if (v.includes('..')) return false; + if (/(?:javascript|data|vbscript)\s*:/i.test(v)) return false; + if (/<[a-z!/]/i.test(v)) return false; + return /^\/?uploads\/[A-Za-z0-9._\-/]+$/.test(v); +} + +export function assertValidDocumentPath(v: string | null | undefined, fieldLabel = 'documentPath'): void { + if (!isValidDocumentPath(v)) { + throw new Error(`${fieldLabel} ist kein gültiger Upload-Pfad (erlaubt: /uploads/).`); + } +} + // Pentest 51.3 + 60.3 (MEDIUM, 2026-06-01): Telefon-/Vorwahl-Felder // dürfen NIE CRLF oder andere Control-Chars enthalten – sonst sind sie // ein Header-Injection-Vektor (Mail, HTTP), wenn der Wert mal in einen