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);
}