security: Audit-Log für alle Klartext-Passwort-Reads (CRITICAL)
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) <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ import { sendEmail, SmtpCredentials, SendEmailParams, EmailAttachment } from '..
|
|||||||
import { fetchAttachment, appendToSent, ImapCredentials } from '../services/imapService.js';
|
import { fetchAttachment, appendToSent, ImapCredentials } from '../services/imapService.js';
|
||||||
import { getImapSmtpSettings } from '../services/emailProvider/emailProviderService.js';
|
import { getImapSmtpSettings } from '../services/emailProvider/emailProviderService.js';
|
||||||
import { decrypt } from '../utils/encryption.js';
|
import { decrypt } from '../utils/encryption.js';
|
||||||
|
import { logChange } from '../services/audit.service.js';
|
||||||
import { ApiResponse } from '../types/index.js';
|
import { ApiResponse } from '../types/index.js';
|
||||||
import { getCustomerTargets, getContractTargets, getIdentityDocumentTargets, getBankCardTargets, documentTargets } from '../config/documentTargets.config.js';
|
import { getCustomerTargets, getContractTargets, getIdentityDocumentTargets, getBankCardTargets, documentTargets } from '../config/documentTargets.config.js';
|
||||||
import { generateEmailPdf } from '../services/pdfService.js';
|
import { generateEmailPdf } from '../services/pdfService.js';
|
||||||
@@ -733,6 +734,15 @@ export async function getMailboxCredentials(req: Request, res: Response): Promis
|
|||||||
// IMAP/SMTP-Einstellungen laden
|
// IMAP/SMTP-Einstellungen laden
|
||||||
const settings = await getImapSmtpSettings();
|
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({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -321,6 +321,14 @@ export async function getContractPassword(req: AuthRequest, res: Response): Prom
|
|||||||
} as ApiResponse);
|
} as ApiResponse);
|
||||||
return;
|
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);
|
res.json({ success: true, data: { password } } as ApiResponse);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({
|
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;
|
if (!(await canAccessContract(req, res, sim.mobileDetails.contractId))) return;
|
||||||
|
|
||||||
const credentials = await contractService.getSimCardCredentials(simCardId);
|
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);
|
res.json({ success: true, data: credentials } as ApiResponse);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
@@ -360,6 +376,14 @@ export async function getInternetCredentials(req: AuthRequest, res: Response): P
|
|||||||
if (!(await canAccessContract(req, res, contractId))) return;
|
if (!(await canAccessContract(req, res, contractId))) return;
|
||||||
|
|
||||||
const credentials = await contractService.getInternetCredentials(contractId);
|
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);
|
res.json({ success: true, data: credentials } as ApiResponse);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({
|
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;
|
if (!(await canAccessContract(req, res, phone.internetDetails.contractId))) return;
|
||||||
|
|
||||||
const credentials = await contractService.getSipCredentials(phoneNumberId);
|
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);
|
res.json({ success: true, data: credentials } as ApiResponse);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
|
|||||||
@@ -986,7 +986,19 @@ export async function setPortalPassword(req: Request, res: Response): Promise<vo
|
|||||||
|
|
||||||
export async function getPortalPassword(req: Request, res: Response): Promise<void> {
|
export async function getPortalPassword(req: Request, res: Response): Promise<void> {
|
||||||
try {
|
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);
|
res.json({ success: true, data: { password } } as ApiResponse);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
|
|||||||
@@ -112,6 +112,13 @@ function determineSensitivity(resourceType: string): AuditSensitivity {
|
|||||||
Authentication: 'CRITICAL',
|
Authentication: 'CRITICAL',
|
||||||
BankCard: 'CRITICAL',
|
BankCard: 'CRITICAL',
|
||||||
IdentityDocument: '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
|
// HIGH
|
||||||
Customer: 'HIGH',
|
Customer: 'HIGH',
|
||||||
User: 'HIGH',
|
User: 'HIGH',
|
||||||
|
|||||||
@@ -97,6 +97,23 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
|
|||||||
|
|
||||||
## ✅ Erledigt
|
## ✅ 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**
|
- [x] **↗ E-Mail-Postfach: Weiterleiten + Erneut senden**
|
||||||
- **Weiterleiten** (Compose-Modal-Erweiterung): neuer Button im
|
- **Weiterleiten** (Compose-Modal-Erweiterung): neuer Button im
|
||||||
EmailDetail öffnet das ComposeEmailModal im Forward-Modus –
|
EmailDetail öffnet das ComposeEmailModal im Forward-Modus –
|
||||||
|
|||||||
Reference in New Issue
Block a user