diff --git a/backend/src/controllers/contract.controller.ts b/backend/src/controllers/contract.controller.ts index 4949a2bb..bba63636 100644 --- a/backend/src/controllers/contract.controller.ts +++ b/backend/src/controllers/contract.controller.ts @@ -18,15 +18,28 @@ import { maybeActivateOnDeliveryConfirmation } from '../services/contractStatusS * und lieferten sie 1:1 an Portal-User zurück. Verträge enthalten KEINE * HTML-Felder (Richtige HTML-Texte liegen in AppSettings), deshalb ist * Strip safe. + * + * AUSNAHME: Passwort-/Secret-Felder. `stripHtml` filtert `<…>`-Sequenzen + * und URI-Schemata wie `data:`, also würde ein PW wie `Password!` + * zu `Password!` mutilieren oder `data:secret` zu `blocked:secret`. + * Das Passwort wird sowieso verschlüsselt persistiert (`encrypt()`), + * niemals als HTML ausgegeben – also kein XSS-Risk, und die Mangling + * ist ein Bug (2026-05-27, intern gemeldet: "Portal-Passwörter werden + * nicht gespeichert"). */ -function sanitizeContractBody(body: unknown): unknown { +const PASSTHROUGH_KEYS = new Set(['portalPassword', 'password']); + +function sanitizeContractBody(body: unknown, parentKey?: string): unknown { if (body === null || body === undefined) return body; - if (typeof body === 'string') return stripHtml(body); - if (Array.isArray(body)) return body.map(sanitizeContractBody); + if (typeof body === 'string') { + if (parentKey && PASSTHROUGH_KEYS.has(parentKey)) return body; + return stripHtml(body); + } + if (Array.isArray(body)) return body.map((v) => sanitizeContractBody(v, parentKey)); if (typeof body === 'object') { const out: Record = {}; for (const [k, v] of Object.entries(body as Record)) { - out[k] = sanitizeContractBody(v); + out[k] = sanitizeContractBody(v, k); } return out; } diff --git a/docs/todo.md b/docs/todo.md index cba16e1c..0b5b8c43 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -120,6 +120,21 @@ 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] **🐛 Bugfix: Portal-Passwörter in Verträgen wurden mutiliert** + - Folgefehler aus Pentest 31.1 (Stored-XSS-Strip): die rekursive + `sanitizeContractBody`-Funktion lief auch über `portalPassword`. + Passwörter mit HTML-Pattern (`Password!` → `Password!`) oder + URI-Schema-Prefix (`data:secret` → `blocked:secret`) wurden + irreparabel zerstört. + - Fix: `PASSTHROUGH_KEYS = {'portalPassword', 'password'}` – beim + Walk werden String-Werte unter diesen Keys NICHT durch + `stripHtml` geschickt. PW wird sowieso `encrypt()`-verschlüsselt + persistiert und niemals als HTML ausgegeben → kein XSS-Risk. + - Live-verifiziert: PW `MyP@ss123!&data:foo` → byte-genau im + GET-Decrypt-Endpoint zurück. `providerName: