security: portalPasswordHash + Encrypted aus embedded customer in /contracts/* entfernen
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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<string, unknown>).customer = sanitizeCustomerStrict(
|
||||
contract.customer as Record<string, unknown>,
|
||||
);
|
||||
}
|
||||
|
||||
// Decrypt password if requested and exists (Contract-Anbieter-Passwort,
|
||||
// nicht zu verwechseln mit Customer-Portal-Passwort)
|
||||
if (decryptPassword && contract.portalPasswordEncrypted) {
|
||||
try {
|
||||
(contract as Record<string, unknown>).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<string, unknown>).customer = sanitizeCustomerStrict(
|
||||
contract.customer as Record<string, unknown>,
|
||||
);
|
||||
}
|
||||
|
||||
return contract;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user