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 prisma from '../lib/prisma.js';
|
||||||
import { generateContractNumber, paginate, buildPaginationResponse } from '../utils/helpers.js';
|
import { generateContractNumber, paginate, buildPaginationResponse } from '../utils/helpers.js';
|
||||||
import { encrypt, decrypt } from '../utils/encryption.js';
|
import { encrypt, decrypt } from '../utils/encryption.js';
|
||||||
|
import { sanitizeCustomerStrict } from '../utils/sanitize.js';
|
||||||
|
|
||||||
export interface ContractFilters {
|
export interface ContractFilters {
|
||||||
customerId?: number;
|
customerId?: number;
|
||||||
@@ -154,7 +155,18 @@ export async function getContractById(id: number, decryptPassword = false) {
|
|||||||
|
|
||||||
if (!contract) return null;
|
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) {
|
if (decryptPassword && contract.portalPasswordEncrypted) {
|
||||||
try {
|
try {
|
||||||
(contract as Record<string, unknown>).portalPasswordDecrypted = decrypt(
|
(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;
|
return contract;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user