diff --git a/backend/src/controllers/contract.controller.ts b/backend/src/controllers/contract.controller.ts index c699d01e..f68b9a96 100644 --- a/backend/src/controllers/contract.controller.ts +++ b/backend/src/controllers/contract.controller.ts @@ -181,6 +181,11 @@ export async function createContract(req: AuthRequest, res: Response): Promise { try { const contractId = parseInt(req.params.id); + // Pentest 56.3 (latent, 2026-06-01): Defense-in-Depth – + // canAccessContract explizit aufrufen, statt sich nur auf die + // Route-Permission zu verlassen. Portal-User mit kompromittierter + // Token-Permission würden sonst beliebige Verträge editieren können. + if (!(await canAccessContract(req, res, contractId))) return; // Vorherigen Stand laden für Audit-Vergleich const before = await prisma.contract.findUnique({ where: { id: contractId }, @@ -264,6 +269,8 @@ export async function updateContract(req: AuthRequest, res: Response): Promise { try { const contractId = parseInt(req.params.id); + // Pentest 56.3 (latent): Defense-in-Depth – Ownership-Check vor Delete. + if (!(await canAccessContract(req as AuthRequest, res, contractId))) return; const contract = await prisma.contract.findUnique({ where: { id: contractId }, select: { contractNumber: true, customerId: true } }); await contractService.deleteContract(contractId); await logChange({ diff --git a/backend/src/controllers/gdpr.controller.ts b/backend/src/controllers/gdpr.controller.ts index 7becc3c9..168f670a 100644 --- a/backend/src/controllers/gdpr.controller.ts +++ b/backend/src/controllers/gdpr.controller.ts @@ -65,7 +65,20 @@ export async function exportCustomerData(req: AuthRequest, res: Response) { */ export async function createDeletionRequest(req: AuthRequest, res: Response) { try { - const customerId = parseInt(req.params.id); + // Pentest 56.5 (LOW, 2026-06-01): customerId muss als gültige Zahl + // aus dem Body kommen (Route hat kein :id-Segment) – und der Caller + // braucht Zugriff auf den Kunden. Ohne den Check konnte jemand mit + // gdpr:delete-Permission Löschanfragen für beliebige Kunden stellen + // (Insider-Sabotage durch Portal-Vertreter ohne Vollmacht). + const bodyCustomerId = req.body?.customerId; + const customerId = typeof bodyCustomerId === 'number' + ? bodyCustomerId + : parseInt(bodyCustomerId); + if (!Number.isFinite(customerId) || customerId < 1) { + res.status(400).json({ success: false, error: 'customerId fehlt oder ungültig' }); + return; + } + if (!(await canAccessCustomer(req, res, customerId))) return; const { requestSource } = req.body; const request = await gdprService.createDeletionRequest({ diff --git a/backend/src/controllers/invoice.controller.ts b/backend/src/controllers/invoice.controller.ts index 06db33e8..6ed25931 100644 --- a/backend/src/controllers/invoice.controller.ts +++ b/backend/src/controllers/invoice.controller.ts @@ -53,6 +53,23 @@ export async function getInvoice(req: AuthRequest, res: Response): Promise /** * Neue Rechnung hinzufügen */ +// Pentest 56.4 (MEDIUM, 2026-06-01): invoiceType wurde an manchen +// Endpunkten nicht gegen die Enum-Whitelist validiert; ein beliebiger +// String landete als invoiceType in der DB und konnte Frontend- +// Filter/Reports verwirren oder XSS in Audit-Labels einschleusen. +type ValidInvoiceType = 'INTERIM' | 'FINAL' | 'NOT_AVAILABLE'; +const VALID_INVOICE_TYPES: Set = new Set(['INTERIM', 'FINAL', 'NOT_AVAILABLE']); +function assertValidInvoiceType(value: unknown, res: Response): value is ValidInvoiceType { + if (typeof value !== 'string' || !VALID_INVOICE_TYPES.has(value as ValidInvoiceType)) { + res.status(400).json({ + success: false, + error: `Ungültiger Rechnungstyp. Erlaubt: ${[...VALID_INVOICE_TYPES].join(', ')}.`, + } as ApiResponse); + return false; + } + return true; +} + export async function addInvoice(req: AuthRequest, res: Response): Promise { try { const ecdId = parseInt(req.params.ecdId); @@ -66,6 +83,7 @@ export async function addInvoice(req: AuthRequest, res: Response): Promise } as ApiResponse); return; } + if (!assertValidInvoiceType(invoiceType, res)) return; const invoice = await invoiceService.addInvoice(ecdId, { invoiceDate: new Date(invoiceDate), @@ -99,6 +117,8 @@ export async function updateInvoice(req: AuthRequest, res: Response): Promise { + const invoice = await prisma.invoice.findUnique({ + where: { id: invoiceId }, + select: { contractId: true, energyContractDetails: { select: { contractId: true } } }, + }); + return invoice?.contractId ?? invoice?.energyContractDetails?.contractId ?? null; +} + +function cleanupFile(filePath?: string) { + if (!filePath) return; + try { fs.unlinkSync(filePath); } catch { /* ignore */ } +} const router = Router(); @@ -164,9 +188,20 @@ router.post( } const bankCardId = parseInt(req.params.id); + // Pentest 56.1: Existenz- und Ownership-Check VOR DB-Update. + const card = await prisma.bankCard.findUnique({ where: { id: bankCardId } }); + if (!card) { + cleanupFile(req.file.path); + res.status(404).json({ success: false, error: 'Bankkarte nicht gefunden' }); + return; + } + if (!(await canAccessBankCard(req, res, bankCardId))) { + cleanupFile(req.file.path); + return; + } + const relativePath = `/uploads/bank-cards/${req.file.filename}`; - // Bankkarte in der DB aktualisieren await prisma.bankCard.update({ where: { id: bankCardId }, data: { documentPath: relativePath }, @@ -183,6 +218,7 @@ router.post( }); } catch (error) { console.error('Upload error:', error); + cleanupFile(req.file?.path); res.status(500).json({ success: false, error: 'Upload fehlgeschlagen' }); } } @@ -204,9 +240,20 @@ router.post( } const documentId = parseInt(req.params.id); + // Pentest 56.1: Existenz- und Ownership-Check VOR DB-Update. + const doc = await prisma.identityDocument.findUnique({ where: { id: documentId } }); + if (!doc) { + cleanupFile(req.file.path); + res.status(404).json({ success: false, error: 'Ausweis nicht gefunden' }); + return; + } + if (!(await canAccessIdentityDocument(req, res, documentId))) { + cleanupFile(req.file.path); + return; + } + const relativePath = `/uploads/documents/${req.file.filename}`; - // Ausweis in der DB aktualisieren await prisma.identityDocument.update({ where: { id: documentId }, data: { documentPath: relativePath }, @@ -223,6 +270,7 @@ router.post( }); } catch (error) { console.error('Upload error:', error); + cleanupFile(req.file?.path); res.status(500).json({ success: false, error: 'Upload fehlgeschlagen' }); } } @@ -246,6 +294,8 @@ router.delete( res.status(404).json({ success: false, error: 'Bankkarte nicht gefunden' }); return; } + // Pentest 56.1: Ownership-Check. + if (!(await canAccessBankCard(req, res, bankCardId))) return; if (!bankCard.documentPath) { res.status(400).json({ success: false, error: 'Kein Dokument vorhanden' }); @@ -290,6 +340,8 @@ router.delete( res.status(404).json({ success: false, error: 'Ausweis nicht gefunden' }); return; } + // Pentest 56.1: Ownership-Check. + if (!(await canAccessIdentityDocument(req, res, documentId))) return; if (!document.documentPath) { res.status(400).json({ success: false, error: 'Kein Dokument vorhanden' }); @@ -334,6 +386,11 @@ router.post( } const customerId = parseInt(req.params.id); + // Pentest 56.1: Ownership-Check. + if (!(await canAccessCustomer(req, res, customerId))) { + cleanupFile(req.file.path); + return; + } const relativePath = `/uploads/business-registrations/${req.file.filename}`; // Alte Datei löschen falls vorhanden @@ -383,6 +440,11 @@ router.post( } const customerId = parseInt(req.params.id); + // Pentest 56.1: Ownership-Check. + if (!(await canAccessCustomer(req, res, customerId))) { + cleanupFile(req.file.path); + return; + } const relativePath = `/uploads/commercial-registers/${req.file.filename}`; // Alte Datei löschen falls vorhanden @@ -424,6 +486,8 @@ router.delete( async (req: AuthRequest, res: Response) => { try { const customerId = parseInt(req.params.id); + // Pentest 56.1: Ownership-Check. + if (!(await canAccessCustomer(req, res, customerId))) return; const customer = await prisma.customer.findUnique({ where: { id: customerId } }); if (!customer) { @@ -464,6 +528,8 @@ router.delete( async (req: AuthRequest, res: Response) => { try { const customerId = parseInt(req.params.id); + // Pentest 56.1: Ownership-Check. + if (!(await canAccessCustomer(req, res, customerId))) return; const customer = await prisma.customer.findUnique({ where: { id: customerId } }); if (!customer) { @@ -514,6 +580,14 @@ router.post( } const customerId = parseInt(req.params.id); + // Pentest 56.2 (HIGH): Ownership-Check VOR Consent-Massen-Update. + // Ohne diese Prüfung konnte jeder Caller mit customers:update für + // jede beliebige customerId ALLE Einwilligungen auf GRANTED setzen + // (DSGVO-Eskalation). + if (!(await canAccessCustomer(req, res, customerId))) { + cleanupFile(req.file.path); + return; + } const relativePath = `/uploads/privacy-policies/${req.file.filename}`; // Alte Datei löschen falls vorhanden @@ -574,6 +648,8 @@ router.delete( async (req: AuthRequest, res: Response) => { try { const customerId = parseInt(req.params.id); + // Pentest 56.1: Ownership-Check. + if (!(await canAccessCustomer(req, res, customerId))) return; const customer = await prisma.customer.findUnique({ where: { id: customerId } }); if (!customer) { @@ -846,14 +922,20 @@ router.post( } const invoiceId = parseInt(req.params.id); - const relativePath = `/uploads/invoices/${req.file.filename}`; - - // Alte Datei löschen falls vorhanden + // Pentest 56.1: Existenz- und Ownership-Check VOR DB-Update. const invoice = await prisma.invoice.findUnique({ where: { id: invoiceId } }); if (!invoice) { + cleanupFile(req.file.path); res.status(404).json({ success: false, error: 'Rechnung nicht gefunden' }); return; } + const invoiceContractId = await resolveInvoiceContractId(invoiceId); + if (invoiceContractId == null || !(await canAccessContract(req, res, invoiceContractId))) { + cleanupFile(req.file.path); + return; + } + + const relativePath = `/uploads/invoices/${req.file.filename}`; if (invoice.documentPath) { const oldPath = path.join(process.cwd(), invoice.documentPath); @@ -898,6 +980,9 @@ router.delete( res.status(404).json({ success: false, error: 'Rechnung nicht gefunden' }); return; } + // Pentest 56.1: Ownership-Check vor Delete. + const invoiceContractId = await resolveInvoiceContractId(invoiceId); + if (invoiceContractId == null || !(await canAccessContract(req, res, invoiceContractId))) return; if (!invoice.documentPath) { res.status(400).json({ success: false, error: 'Kein Dokument vorhanden' });