From 0943f11999ba9fc4b8d338def05b848349483eda Mon Sep 17 00:00:00 2001 From: duffyduck Date: Sat, 16 May 2026 15:33:26 +0200 Subject: [PATCH] =?UTF-8?q?security:=20Audit-Log=20f=C3=BCr=20alle=20Klart?= =?UTF-8?q?ext-Passwort-Reads=20(CRITICAL)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pentest-Finding "Klartext-Passwörter über API abrufbar (HIGH, post-auth)" adressiert: reversible Verschlüsselung der Anbieter-/Portal-Logins ist by-design (Feature "Login anzeigen" braucht sie zwingend), aber jeder einzelne Decrypt-Vorgang muss im Audit-Log nachvollziehbar sein. Bisher schrieb KEINER der 6 betroffenen Endpoints einen Eintrag. Behoben in: - getPortalPassword (Customer-Portal-Login) - getContractPassword (Anbieter-Login z.B. Vattenfall, EWE, …) - getSimCardCredentials (PIN/PUK) - getInternetCredentials (DSL-Login) - getSipCredentials (Telefon-/VoIP-Login) - getMailboxCredentials (Stressfrei-IMAP/SMTP) Alle nutzen `action: 'READ'` mit eigenem ResourceType + Sensitivity CRITICAL via determineSensitivity-Map. Label nennt explizit "Klartext … entschlüsselt" + Resource-ID, damit im AuditLog-Viewer auf einen Blick erkennbar ist, wer wann welches Passwort eingesehen hat (DSGVO + Insider-Threat-Erkennung). Live verifiziert: nach Klick auf getPortalPassword erscheint im AuditLog der Eintrag "READ PortalPassword CRITICAL – Klartext-Portal- Passwort von Kunde #1 entschlüsselt". Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/controllers/cachedEmail.controller.ts | 10 ++++++ .../src/controllers/contract.controller.ts | 32 +++++++++++++++++++ .../src/controllers/customer.controller.ts | 14 +++++++- backend/src/services/audit.service.ts | 7 ++++ docs/todo.md | 17 ++++++++++ 5 files changed, 79 insertions(+), 1 deletion(-) diff --git a/backend/src/controllers/cachedEmail.controller.ts b/backend/src/controllers/cachedEmail.controller.ts index 61ce7f34..c3713527 100644 --- a/backend/src/controllers/cachedEmail.controller.ts +++ b/backend/src/controllers/cachedEmail.controller.ts @@ -8,6 +8,7 @@ import { sendEmail, SmtpCredentials, SendEmailParams, EmailAttachment } from '.. import { fetchAttachment, appendToSent, ImapCredentials } from '../services/imapService.js'; import { getImapSmtpSettings } from '../services/emailProvider/emailProviderService.js'; import { decrypt } from '../utils/encryption.js'; +import { logChange } from '../services/audit.service.js'; import { ApiResponse } from '../types/index.js'; import { getCustomerTargets, getContractTargets, getIdentityDocumentTargets, getBankCardTargets, documentTargets } from '../config/documentTargets.config.js'; import { generateEmailPdf } from '../services/pdfService.js'; @@ -733,6 +734,15 @@ export async function getMailboxCredentials(req: Request, res: Response): Promis // IMAP/SMTP-Einstellungen laden const settings = await getImapSmtpSettings(); + // Klartext-Mailbox-Passwort-Read auditieren (CRITICAL) + await logChange({ + req, + action: 'READ', + resourceType: 'MailboxCredentials', + resourceId: id.toString(), + label: `Klartext-Mailbox-Zugangsdaten von ${stressfreiEmail.email} entschlüsselt`, + }); + res.json({ success: true, data: { diff --git a/backend/src/controllers/contract.controller.ts b/backend/src/controllers/contract.controller.ts index 9ced51d6..c025d534 100644 --- a/backend/src/controllers/contract.controller.ts +++ b/backend/src/controllers/contract.controller.ts @@ -321,6 +321,14 @@ export async function getContractPassword(req: AuthRequest, res: Response): Prom } as ApiResponse); return; } + // Klartext-Passwort-Read auditieren (CRITICAL) + await logChange({ + req, + action: 'READ', + resourceType: 'ContractPassword', + resourceId: contractId.toString(), + label: `Klartext-Anbieter-Passwort von Vertrag #${contractId} entschlüsselt`, + }); res.json({ success: true, data: { password } } as ApiResponse); } catch (error) { res.status(500).json({ @@ -345,6 +353,14 @@ export async function getSimCardCredentials(req: AuthRequest, res: Response): Pr if (!(await canAccessContract(req, res, sim.mobileDetails.contractId))) return; const credentials = await contractService.getSimCardCredentials(simCardId); + // Klartext-Read (PIN/PUK) auditieren (CRITICAL) + await logChange({ + req, + action: 'READ', + resourceType: 'SimCardCredentials', + resourceId: simCardId.toString(), + label: `Klartext-SIM-Karten-PIN/PUK von SIM #${simCardId} (Vertrag #${sim.mobileDetails.contractId}) entschlüsselt`, + }); res.json({ success: true, data: credentials } as ApiResponse); } catch (error) { res.status(500).json({ @@ -360,6 +376,14 @@ export async function getInternetCredentials(req: AuthRequest, res: Response): P if (!(await canAccessContract(req, res, contractId))) return; const credentials = await contractService.getInternetCredentials(contractId); + // Klartext-DSL/Internet-Login auditieren (CRITICAL) + await logChange({ + req, + action: 'READ', + resourceType: 'InternetCredentials', + resourceId: contractId.toString(), + label: `Klartext-Internet-Zugangsdaten von Vertrag #${contractId} entschlüsselt`, + }); res.json({ success: true, data: credentials } as ApiResponse); } catch (error) { res.status(500).json({ @@ -384,6 +408,14 @@ export async function getSipCredentials(req: AuthRequest, res: Response): Promis if (!(await canAccessContract(req, res, phone.internetDetails.contractId))) return; const credentials = await contractService.getSipCredentials(phoneNumberId); + // Klartext-SIP/Telefon-Login auditieren (CRITICAL) + await logChange({ + req, + action: 'READ', + resourceType: 'SipCredentials', + resourceId: phoneNumberId.toString(), + label: `Klartext-SIP-Zugangsdaten von Rufnummer #${phoneNumberId} (Vertrag #${phone.internetDetails.contractId}) entschlüsselt`, + }); res.json({ success: true, data: credentials } as ApiResponse); } catch (error) { res.status(500).json({ diff --git a/backend/src/controllers/customer.controller.ts b/backend/src/controllers/customer.controller.ts index 307bd2e9..66dc9273 100644 --- a/backend/src/controllers/customer.controller.ts +++ b/backend/src/controllers/customer.controller.ts @@ -986,7 +986,19 @@ export async function setPortalPassword(req: Request, res: Response): Promise { try { - const password = await authService.getCustomerPortalPassword(parseInt(req.params.customerId)); + const customerId = parseInt(req.params.customerId); + const password = await authService.getCustomerPortalPassword(customerId); + // Klartext-Passwort-Read auditieren (CRITICAL): wer hat wann das Portal- + // Passwort eines Kunden entschlüsselt? Wichtig für DSGVO-Nachvollziehbarkeit + // + Insider-Threat-Erkennung. + await logChange({ + req, + action: 'READ', + resourceType: 'PortalPassword', + resourceId: customerId.toString(), + label: `Klartext-Portal-Passwort von Kunde #${customerId} entschlüsselt`, + customerId, + }); res.json({ success: true, data: { password } } as ApiResponse); } catch (error) { res.status(500).json({ diff --git a/backend/src/services/audit.service.ts b/backend/src/services/audit.service.ts index 44c0474a..55d34099 100644 --- a/backend/src/services/audit.service.ts +++ b/backend/src/services/audit.service.ts @@ -112,6 +112,13 @@ function determineSensitivity(resourceType: string): AuditSensitivity { Authentication: 'CRITICAL', BankCard: 'CRITICAL', IdentityDocument: 'CRITICAL', + // Klartext-Passwort-Reads – jeder Decrypt-Vorgang muss nachvollziehbar sein + PortalPassword: 'CRITICAL', + ContractPassword: 'CRITICAL', + SimCardCredentials: 'CRITICAL', + InternetCredentials: 'CRITICAL', + SipCredentials: 'CRITICAL', + MailboxCredentials: 'CRITICAL', // HIGH Customer: 'HIGH', User: 'HIGH', diff --git a/docs/todo.md b/docs/todo.md index a2c90dbc..f15caa6d 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -97,6 +97,23 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung ## ✅ Erledigt +- [x] **🔒 Audit-Log für alle Klartext-Passwort-Reads** + - Pentest-Finding „Klartext-Passwörter über API abrufbar (HIGH, + post-auth)" → reversible Verschlüsselung ist by-design (Feature + „Anbieter-Login anzeigen" braucht es), aber jeder Decrypt-Vorgang + sollte im Audit-Log auftauchen. Bisher: keiner der 6 Endpoints + schrieb ein Log. + - Audit-Logs jetzt für: `getPortalPassword`, `getContractPassword`, + `getSimCardCredentials`, `getInternetCredentials`, + `getSipCredentials`, `getMailboxCredentials`. + - `action: 'READ'`, eigene Resource-Types (PortalPassword, + ContractPassword, SimCardCredentials, InternetCredentials, + SipCredentials, MailboxCredentials), alle mit `sensitivity: + CRITICAL` über die Sensitivity-Map. + - Label nennt explizit „Klartext … entschlüsselt" + Ressourcen-ID, + damit im Audit-Log-Viewer auf einen Blick erkennbar ist, was + passiert ist (DSGVO-Nachvollziehbarkeit + Insider-Threat-Erkennung). + - [x] **↗ E-Mail-Postfach: Weiterleiten + Erneut senden** - **Weiterleiten** (Compose-Modal-Erweiterung): neuer Button im EmailDetail öffnet das ComposeEmailModal im Forward-Modus –