From b3a6620da6ba9437e8483962d8e33b3bc441eeff Mon Sep 17 00:00:00 2001 From: duffyduck Date: Tue, 19 May 2026 12:49:19 +0200 Subject: [PATCH] =?UTF-8?q?XSS-Sanitization=20f=C3=BCr=20AppSettings=20(co?= =?UTF-8?q?mpanyName=20&=20Co)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pentest-Befund (MEDIUM): companyName und weitere Plain-Text-Setting- Keys nahmen via PUT /api/settings/:key XSS-Payloads wie ungefiltert entgegen. Nur Admin triggerbar, aber E-Mail-Templates/PDF-Generatoren hätten den Wert unescaped rendern können. Fix in appSetting.service.ts: sanitizeSettingValue(key, value) strippt HTML außer für die expliziten Editor-Keys (imprintHtml, privacyPolicyHtml, authorizationTemplateHtml, websitePrivacyPolicyHtml). Greift in updateSetting + updateSettings. cleanup-xss-and-mass-assignment.ts bereinigt bestehende dreckige Werte beim Container-Start (idempotent). Live-verifiziert auf dev: - PUT companyName="OpenCRM" → DB: "OpenCRM" - Bulk-PUT mit XSS auf companyName + defaultEmailDomain → gestrippt - imprintHtml mit "

...

" → unverändert (HTML-allowed) - Cleanup-Skript auf dirty value: "EvilCo" statt mit Tags Co-Authored-By: Claude Opus 4.7 (1M context) --- .../prisma/cleanup-xss-and-mass-assignment.ts | 29 +++++++++++++++++++ .../src/controllers/appSetting.controller.ts | 7 +++-- backend/src/services/appSetting.service.ts | 27 +++++++++++++++++ docs/todo.md | 18 ++++++++++++ 4 files changed, 79 insertions(+), 2 deletions(-) diff --git a/backend/prisma/cleanup-xss-and-mass-assignment.ts b/backend/prisma/cleanup-xss-and-mass-assignment.ts index 4d72f997..2b5366c9 100644 --- a/backend/prisma/cleanup-xss-and-mass-assignment.ts +++ b/backend/prisma/cleanup-xss-and-mass-assignment.ts @@ -63,16 +63,45 @@ async function cleanupXss() { 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, ''); +} + async function cleanupAppSettings() { const settings = await prisma.appSetting.findMany(); const removed: string[] = []; + let stripped = 0; for (const s of settings) { if (!ALLOWED_SETTING_KEYS.has(s.key)) { removed.push(s.key); await prisma.appSetting.delete({ where: { key: s.key } }); + continue; + } + if (!HTML_ALLOWED_SETTING_KEYS.has(s.key)) { + const cleaned = stripHtmlString(s.value); + if (cleaned !== s.value) { + await prisma.appSetting.update({ where: { key: s.key }, data: { value: cleaned } }); + stripped++; + } } } console.log(` → AppSettings entfernt: ${removed.length}${removed.length ? ' (' + removed.join(', ') + ')' : ''}`); + if (stripped > 0) { + console.log(` → AppSettings HTML-gestrippt: ${stripped}`); + } } // Pattern, die auf typische Pentest-/Test-Daten hindeuten. Bewusst eng diff --git a/backend/src/controllers/appSetting.controller.ts b/backend/src/controllers/appSetting.controller.ts index 9ee535af..0e88497c 100644 --- a/backend/src/controllers/appSetting.controller.ts +++ b/backend/src/controllers/appSetting.controller.ts @@ -53,7 +53,10 @@ export async function updateSetting(req: AuthRequest, res: Response): Promise = { @@ -33,6 +34,32 @@ export function isAllowedSettingKey(key: string): boolean { return ALLOWED_SETTING_KEYS.has(key); } +// Keys deren Wert legitim HTML enthalten darf (Datenschutz-/Impressum-Editoren +// liefern WYSIWYG-HTML). Alle anderen Plain-Text-Keys (companyName, +// defaultEmailDomain, Schwellenwerte etc.) werden vor dem Persistieren durch +// stripHtml geschickt – Pentest 2026-05-19, MEDIUM: +// in companyName landete ungefiltert in der DB und konnte später z.B. in +// E-Mail-Templates oder PDF-Generatoren unescaped landen. +const HTML_ALLOWED_SETTING_KEYS: ReadonlySet = new Set([ + 'authorizationTemplateHtml', + 'imprintHtml', + 'privacyPolicyHtml', + 'websitePrivacyPolicyHtml', +]); + +/** + * Bereinigt den Wert vor dem Speichern: für Plain-Text-Keys werden alle + * HTML-Tags entfernt. Die dedizierten Editor-Keys + * (imprintHtml/privacyPolicyHtml/...) bleiben unverändert, da sie sonst + * den WYSIWYG-Editor unbenutzbar machen würden – sie werden über + * dedizierte /api/gdpr-Endpoints gepflegt. + */ +export function sanitizeSettingValue(key: string, value: string): string { + if (HTML_ALLOWED_SETTING_KEYS.has(key)) return value; + const stripped = stripHtml(value); + return typeof stripped === 'string' ? stripped : String(stripped); +} + export async function getSetting(key: string): Promise { const setting = await prisma.appSetting.findUnique({ where: { key }, diff --git a/docs/todo.md b/docs/todo.md index fba2fcb2..efeaab22 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -120,6 +120,24 @@ 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] **🛡️ XSS-Sanitization für Plain-Text-AppSettings (Pentest MEDIUM)** + - `companyName` (und weitere Plain-Text-Keys wie `defaultEmailDomain`, + `monitoringAlertEmail`, Schwellenwerte) konnten via PUT + `/api/settings/:key` mit XSS-Payloads befüllt werden – das war + nur Admin-triggerbar, aber E-Mail-Templates/PDF-Generatoren + hätten den Wert ungescaped rendern können. + - Fix: neuer `sanitizeSettingValue(key, value)` in + `appSetting.service.ts` strippt HTML außer für die expliziten + HTML-Editor-Keys (`imprintHtml`, `privacyPolicyHtml`, + `authorizationTemplateHtml`, `websitePrivacyPolicyHtml`). + Greift in `updateSetting` (Einzel) und `updateSettings` (Bulk). + - Cleanup-Skript erweitert: bestehende AppSettings mit HTML in + Plain-Text-Keys werden beim Container-Start gestrippt + (idempotent). + - **Live-verifiziert** auf dev: `OpenCRM + ` via PUT → DB-Wert: `"OpenCRM"`. + `imprintHtml` mit `

` → unverändert. + - [x] **🐛 Rollen-Perms-Sync beim Container-Start (Follow-up DSGVO-Fix)** - Bestehende Installationen liefen weiter mit veraltetem Permission-Set für die DSGVO-Rolle (audit:read u.a. fehlten),