/** * Einmal-Bereinigung für Pentest-Reste (Runde 12 / 2026-05-18): * * 1. HTML-Tags aus Customer/User-Stringfeldern strippen (M2-Stored-XSS-Reste) * 2. Unbekannte App-Settings entfernen, die durch Mass-Assignment in die DB * gerutscht sind, BEVOR die Whitelist eingezogen wurde (M1-Reste). * * Idempotent: wenn nichts zu tun ist, ändert sich nichts. Bei Bedarf * mehrfach aufrufbar. */ import prisma from '../src/lib/prisma.js'; import { stripHtml } from '../src/utils/sanitize.js'; import { ALLOWED_SETTING_KEYS } from '../src/services/appSetting.service.js'; const CUSTOMER_STRING_FIELDS = [ 'salutation', 'firstName', 'lastName', 'companyName', 'birthPlace', 'email', 'phone', 'mobile', 'taxNumber', 'commercialRegisterNumber', 'notes', ]; const USER_STRING_FIELDS = [ 'firstName', 'lastName', 'email', 'whatsappNumber', 'telegramUsername', 'signalNumber', ]; async function cleanupXss() { const customers = await prisma.customer.findMany(); let touched = 0; for (const c of customers) { const updates: Record = {}; for (const field of CUSTOMER_STRING_FIELDS) { const v = (c as any)[field]; if (typeof v === 'string') { const cleaned = stripHtml(v) as string; if (cleaned !== v) updates[field] = cleaned; } } if (Object.keys(updates).length > 0) { console.log(` Customer #${c.id}: bereinigt:`, Object.keys(updates).join(', ')); await prisma.customer.update({ where: { id: c.id }, data: updates }); touched++; } } console.log(` → Customer bereinigt: ${touched}`); const users = await prisma.user.findMany(); let userTouched = 0; for (const u of users) { const updates: Record = {}; for (const field of USER_STRING_FIELDS) { const v = (u as any)[field]; if (typeof v === 'string') { const cleaned = stripHtml(v) as string; if (cleaned !== v) updates[field] = cleaned; } } if (Object.keys(updates).length > 0) { console.log(` User #${u.id}: bereinigt:`, Object.keys(updates).join(', ')); await prisma.user.update({ where: { id: u.id }, data: updates }); userTouched++; } } console.log(` → User bereinigt: ${userTouched}`); } // HTML in Plain-Text-Settings strippen: WYSIWYG-Editoren liefern // absichtlich HTML, alles andere (companyName, defaultEmailDomain, ...) // muss reiner Text bleiben. Pentest 2026-05-19, MEDIUM. const HTML_ALLOWED_SETTING_KEYS = new Set([ 'authorizationTemplateHtml', 'imprintHtml', 'privacyPolicyHtml', 'websitePrivacyPolicyHtml', ]); function stripHtmlString(s: string): string { return s .replace(/]*>[\s\S]*?<\/script>/gi, '') .replace(/]*>[\s\S]*?<\/style>/gi, '') .replace(/<\/?[a-z][^>]*>/gi, '') .replace(/(?:javascript|data|vbscript)\s*:/gi, 'blocked:'); } // Legitime CustomerConsent.source-Werte. Alles andere wird beim Cleanup // auf 'unknown' normalisiert. Pentest 2026-05-20. const ALLOWED_CONSENT_SOURCES = new Set([ 'portal', 'public-link', 'telefon', 'papier', 'email', 'crm-backend', ]); // 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); } async function cleanupConsents() { // version + documentPath: HTML strippen (waren ohne Validierung). // source: Whitelist erzwingen. // documentPath zusätzlich gegen Pfad-Traversal absichern (27.1). let versionStripped = 0; let pathNulled = 0; let sourceFixed = 0; const consents = await prisma.customerConsent.findMany({ select: { id: true, source: true, documentPath: true, version: true }, }); for (const c of consents) { const data: Record = {}; if (c.version && c.version !== stripHtmlString(c.version)) { data.version = stripHtmlString(c.version); versionStripped++; } if (c.documentPath && !isValidDocumentPath(c.documentPath)) { // ".../etc/passwd", "