diff --git a/backend/src/services/contract.service.ts b/backend/src/services/contract.service.ts index 3d266337..b0182b47 100644 --- a/backend/src/services/contract.service.ts +++ b/backend/src/services/contract.service.ts @@ -2,6 +2,7 @@ import { ContractType, ContractStatus } from '@prisma/client'; import prisma from '../lib/prisma.js'; import { generateContractNumber, paginate, buildPaginationResponse } from '../utils/helpers.js'; import { encrypt, decrypt } from '../utils/encryption.js'; +import { sanitizeCustomerStrict } from '../utils/sanitize.js'; export interface ContractFilters { customerId?: number; @@ -154,7 +155,18 @@ export async function getContractById(id: number, decryptPassword = false) { if (!contract) return null; - // Decrypt password if requested and exists + // SECURITY: Embedded Customer-Objekt sanitizen, sonst leaken + // portalPasswordHash + portalPasswordEncrypted + Reset-Token in jede + // contract.customer-Response. Der direkte `/customers/:id`-Endpoint hat + // den Schutz schon; hier wäre er ohne Sanitize bypassbar. + if (contract.customer) { + (contract as Record).customer = sanitizeCustomerStrict( + contract.customer as Record, + ); + } + + // Decrypt password if requested and exists (Contract-Anbieter-Passwort, + // nicht zu verwechseln mit Customer-Portal-Passwort) if (decryptPassword && contract.portalPasswordEncrypted) { try { (contract as Record).portalPasswordDecrypted = decrypt( @@ -385,6 +397,15 @@ export async function createContract(data: ContractCreateData) { }, }); + // Embedded Customer-Objekt sanitizen (siehe getContractById – derselbe + // Schutz; createContract gibt den frisch erstellten Vertrag inkl. Customer + // zurück, und der darf keine Passwort-Hashes/-Encryptions leaken). + if (contract.customer) { + (contract as Record).customer = sanitizeCustomerStrict( + contract.customer as Record, + ); + } + return contract; }