From 92d2e62e797b7b21a963a82f077df80ec8c05c57 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Sat, 16 May 2026 18:06:01 +0200 Subject: [PATCH] security: portalPasswordHash + Encrypted aus embedded customer in /contracts/* entfernen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Folgefix zum CRITICAL-IDOR auf Stressfrei-Sub-Routes: der separate /customers/:id-Endpoint sanitizt seinen Output schon, aber GET /contracts/:id embeddete weiterhin das volle Customer-Objekt inkl. - portalPasswordHash (bcrypt-Hash des Portal-Login-Passworts) - portalPasswordEncrypted (AES-256-GCM des Klartext-Passworts) - portalPasswordResetToken (langlebiger 1-time-Token) Zwei Lecks im contract.service: - getContractById hatte `customer: true` ohne Sanitize - createContract hatte dasselbe Muster Beide jetzt mit sanitizeCustomerStrict() nach dem Load. Der Helper war schon im utils/sanitize.ts vorhanden – wurde nur nicht aufgerufen. Live-verifiziert: GET /api/contracts/1 → embedded customer enthält 30 saubere Felder, KEIN portalPasswordHash/Encrypted/ResetToken mehr. Weitere `customer: true`-Stellen geprüft und freigegeben: - pdfTemplate.service.generateFilledPdf: nur internal, gibt PDF-Buffer zurück - cachedEmail.controller.saveEmailAsPdf: nur internal für File-Ops - getAllContracts: schon mit explizitem Select (5 sichere Felder) - updateContract: kein customer-Include Co-Authored-By: Claude Opus 4.7 (1M context) --- backend/src/services/contract.service.ts | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) 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; }