diff --git a/backend/prisma/cleanup-xss-and-mass-assignment.ts b/backend/prisma/cleanup-xss-and-mass-assignment.ts index 2b5366c9..2b7ad8ef 100644 --- a/backend/prisma/cleanup-xss-and-mass-assignment.ts +++ b/backend/prisma/cleanup-xss-and-mass-assignment.ts @@ -77,7 +77,52 @@ function stripHtmlString(s: string): string { return s .replace(/]*>[\s\S]*?<\/script>/gi, '') .replace(/]*>[\s\S]*?<\/style>/gi, '') - .replace(/<\/?[a-z][^>]*>/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', +]); + +async function cleanupConsents() { + // version + documentPath: HTML strippen (waren ohne Validierung). + // source: Whitelist erzwingen. + let versionStripped = 0; + let pathStripped = 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 && c.documentPath !== stripHtmlString(c.documentPath)) { + data.documentPath = stripHtmlString(c.documentPath); + pathStripped++; + } + if (c.source && !ALLOWED_CONSENT_SOURCES.has(c.source)) { + data.source = 'unknown'; + sourceFixed++; + } + if (Object.keys(data).length > 0) { + await prisma.customerConsent.update({ where: { id: c.id }, data }); + } + } + console.log( + ` → Consent bereinigt: version-stripped=${versionStripped}, ` + + `documentPath-stripped=${pathStripped}, source-whitelist=${sourceFixed}`, + ); } async function cleanupAppSettings() { @@ -182,6 +227,7 @@ async function main() { console.log('=== Cleanup: XSS-Reste + Mass-Assignment-AppSettings ==='); await cleanupXss(); await cleanupAppSettings(); + await cleanupConsents(); await findOrPurgePentestRecords(); console.log('=== Fertig. ==='); } diff --git a/backend/src/controllers/gdpr.controller.ts b/backend/src/controllers/gdpr.controller.ts index dda65d2f..f192a492 100644 --- a/backend/src/controllers/gdpr.controller.ts +++ b/backend/src/controllers/gdpr.controller.ts @@ -13,6 +13,7 @@ import fs from 'fs'; import { sendEmail, SmtpCredentials } from '../services/smtpService.js'; import { getSystemEmailCredentials } from '../services/emailProvider/emailProviderService.js'; import * as authorizationService from '../services/authorization.service.js'; +import { stripHtml } from '../utils/sanitize.js'; /** * Kundendaten exportieren (DSGVO Art. 15) @@ -269,7 +270,14 @@ export async function updateCustomerConsent(req: AuthRequest, res: Response) { try { const customerId = parseInt(req.params.customerId); const consentType = req.params.consentType as ConsentType; - const { status, source, documentPath, version } = req.body; + // BEWUSST nur `status` aus dem Body übernehmen. `source`, `documentPath` + // und `version` darf der Portal-User NICHT setzen – Pentest 2026-05-20 + // (MEDIUM): "ADMIN_OVERRIDE" als source bzw. " in * companyName landete vorher ungefiltert in der DB. + * + * Pentest 2026-05-20 (LOW): zusätzlich werden Skript-URI-Schemata + * unschädlich gemacht (`javascript:`, `data:`, `vbscript:`). Plain-Text- + * Felder enthalten legitime URLs ohnehin selten; ein gespeicherter + * `javascript:alert(1)` würde ansonsten in einem `` + * sofort feuern. */ +const DANGEROUS_URI_SCHEMES = /(?:javascript|data|vbscript)\s*:/gi; + export function stripHtml(value: unknown): unknown { if (typeof value !== 'string') return value; return value .replace(/]*>[\s\S]*?<\/script>/gi, '') .replace(/]*>[\s\S]*?<\/style>/gi, '') - .replace(/<\/?[a-z][^>]*>/gi, ''); + .replace(/<\/?[a-z][^>]*>/gi, '') + // Schema durch harmloses Token ersetzen – komplette Entfernung + // könnte legitimen Text wie "Java Script :)" verändern, dieses + // Pattern matcht nur das Schema selbst. + .replace(DANGEROUS_URI_SCHEMES, 'blocked:'); } /** diff --git a/docs/todo.md b/docs/todo.md index 8a8b45a5..e0af2329 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -120,6 +120,34 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung - **Live-verifiziert**: 4867 Datensätze + 1 Datei in 13.2s wiederhergestellt, Log-Modal zeigt den vollständigen Verlauf. +- [x] **🛡️ Pentest 2026-05-20 MEDIUM+LOW: Consent + URI-Sanitization** + - **MEDIUM Consent-Mass-Assignment**: PUT `/api/gdpr/customer/:id/consents/:type` + nahm `source`, `documentPath`, `version` ungefiltert aus dem Body + – Portal-User konnte `source: "ADMIN_OVERRIDE"`, `version: + "