diff --git a/backend/src/utils/sanitize.ts b/backend/src/utils/sanitize.ts index 3e382415..9a0294da 100644 --- a/backend/src/utils/sanitize.ts +++ b/backend/src/utils/sanitize.ts @@ -59,6 +59,39 @@ const PORTAL_HIDDEN_CONTRACT_FIELDS = [ 'nextReviewDate', // Snooze-Workflow ist internes Cockpit-Feature ] as const; +// User-eingabe String-Felder am Contract, die in der UI dargestellt werden. +// Werden beim Read über stripHtml geschickt, damit Alt-Daten mit rohen +// XSS-Payloads (vor Einführung von sanitizeContractBody) nicht mehr als +// `` in der Liste auftauchen. Neue Daten sind +// schon beim Write gestrippt, aber doppelt hält besser. +const CONTRACT_DISPLAY_STRING_FIELDS = [ + 'providerName', + 'tariffName', + 'customerNumberAtProvider', + 'contractNumberAtProvider', + 'portalUsername', + 'previousProviderName', + 'previousCustomerNumber', + 'previousContractNumber', + 'notes', +] as const; + +// User-eingabe String-Felder am Customer für dieselbe Read-Time-Defensive. +const CUSTOMER_DISPLAY_STRING_FIELDS = [ + 'firstName', + 'lastName', + 'companyName', + 'salutation', + 'email', + 'phone', + 'mobile', + 'portalEmail', + 'portalUsername', + 'taxNumber', + 'commercialRegisterNumber', + 'notes', +] as const; + const SENSITIVE_USER_FIELDS = [ 'password', 'passwordResetToken', @@ -79,6 +112,11 @@ export function sanitizeCustomer>(customer: T for (const field of SENSITIVE_CUSTOMER_FIELDS) { delete copy[field]; } + for (const field of CUSTOMER_DISPLAY_STRING_FIELDS) { + if (typeof copy[field] === 'string') { + copy[field] = stripHtml(copy[field]); + } + } if (Array.isArray(copy.contracts)) { copy.contracts = (copy.contracts as Record[]).map((c) => sanitizeContract(c)); } @@ -126,6 +164,25 @@ export function sanitizeContract>(contract: T for (const field of SENSITIVE_CONTRACT_FIELDS) { delete copy[field]; } + for (const field of CONTRACT_DISPLAY_STRING_FIELDS) { + if (typeof copy[field] === 'string') { + copy[field] = stripHtml(copy[field]); + } + } + // Nested: previousProviderName liegt im energyDetails-Sub-Objekt + if (copy.energyDetails && typeof copy.energyDetails === 'object') { + const ed = copy.energyDetails as Record; + if (typeof ed.previousProviderName === 'string') { + ed.previousProviderName = stripHtml(ed.previousProviderName); + } + if (typeof ed.previousCustomerNumber === 'string') { + ed.previousCustomerNumber = stripHtml(ed.previousCustomerNumber); + } + } + // Nested: previousContract wird rekursiv auch sanitisiert + if (copy.previousContract && typeof copy.previousContract === 'object') { + copy.previousContract = sanitizeContract(copy.previousContract as Record); + } if (copy.customer && typeof copy.customer === 'object') { copy.customer = sanitizeCustomer(copy.customer as Record); }