aa0900410b
31.1 Stored XSS in Vertragsfeldern: providerName, tariffName, priceFirst12Months, priceFrom13Months, priceAfter24Months nahmen rohe HTML-/Script-Payloads (<script>, <svg/onload>, <img onerror>, javascript:, HTML-Entities) an und lieferten sie 1:1 an Portal-User zurueck. Fix: rekursiver sanitizeContractBody()-Walker im contract.controller, strippt String-Werte ueber das bestehende stripHtml() (Tag-Strip + URI-Schema-Block + Entity-Decode). Verträge enthalten keine legitimen HTML-Felder, deshalb safe. Audit-Vergleich nutzt jetzt die sanitisierte Variante, sonst Audit ↔ DB-Drift. 31.2 IDOR auf GET /api/customers/:id/stressfrei-emails (+5 weitere): requireCustomerAccess short-circuitete auf customers:read. Portal- User haben aber genau diese Perm im JWT (für eigene Daten) – damit kam Portal-Kunde 1 an Adressen/Bank-Cards/Documents/Meters/ Stressfrei-Emails von Kunde 3. Fix im Middleware: erst isCustomerPortal-Check (eigene + vertretene IDs), DANN erst Perm-Check für Mitarbeiter. Mit einem Patch alle sechs requireCustomerAccess-Routes dicht. Defense-in-Depth: zusätzlicher canAccessCustomer-Call in stressfreiEmail.getEmailsByCustomer analog zum POST-Handler. Live-verifiziert auf dev: - Portal-User 1 → Customer 3: alle 6 Routes 403 - XSS-Payloads in 5 Contract-Feldern → DB enthält bereinigte Werte Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
178 lines
6.4 KiB
TypeScript
178 lines
6.4 KiB
TypeScript
import { Request, Response } from 'express';
|
||
import * as stressfreiEmailService from '../services/stressfreiEmail.service.js';
|
||
import { logChange } from '../services/audit.service.js';
|
||
import { ApiResponse, AuthRequest } from '../types/index.js';
|
||
import { canAccessCustomer, canAccessStressfreiEmail } from '../utils/accessControl.js';
|
||
|
||
export async function getEmailsByCustomer(req: AuthRequest, res: Response): Promise<void> {
|
||
try {
|
||
const customerId = parseInt(req.params.customerId);
|
||
// requireCustomerAccess in der Route greift nicht ausreichend:
|
||
// Portal-User haben `customers:read` (für eigene Daten) und werden
|
||
// dort short-circuited, ohne Owner-Vergleich. Pentest 2026-05-24
|
||
// (MEDIUM 31.2) – IDOR auf fremde IMAP-Konten. Hier daher der
|
||
// explizite Per-Customer-Check analog zum POST-Handler.
|
||
if (!(await canAccessCustomer(req, res, customerId))) return;
|
||
const includeInactive = req.query.includeInactive === 'true';
|
||
const emails = await stressfreiEmailService.getEmailsByCustomerId(customerId, includeInactive);
|
||
res.json({ success: true, data: emails } as ApiResponse);
|
||
} catch (error) {
|
||
res.status(500).json({
|
||
success: false,
|
||
error: 'Fehler beim Laden der Stressfrei-Wechseln Adressen',
|
||
} as ApiResponse);
|
||
}
|
||
}
|
||
|
||
export async function getEmail(req: AuthRequest, res: Response): Promise<void> {
|
||
try {
|
||
const emailId = parseInt(req.params.id);
|
||
if (!(await canAccessStressfreiEmail(req, res, emailId))) return;
|
||
|
||
const email = await stressfreiEmailService.getEmailById(emailId);
|
||
if (!email) {
|
||
res.status(404).json({
|
||
success: false,
|
||
error: 'Stressfrei-Wechseln Adresse nicht gefunden',
|
||
} as ApiResponse);
|
||
return;
|
||
}
|
||
|
||
// Sensibles Feld emailPasswordEncrypted nie an Portal-Kunden geben
|
||
const sanitized: any = { ...email };
|
||
if (req.user?.isCustomerPortal) {
|
||
delete sanitized.emailPasswordEncrypted;
|
||
}
|
||
res.json({ success: true, data: sanitized } as ApiResponse);
|
||
} catch (error) {
|
||
res.status(500).json({
|
||
success: false,
|
||
error: 'Fehler beim Laden der Stressfrei-Wechseln Adresse',
|
||
} as ApiResponse);
|
||
}
|
||
}
|
||
|
||
export async function createEmail(req: Request, res: Response): Promise<void> {
|
||
try {
|
||
const customerId = parseInt(req.params.customerId);
|
||
const email = await stressfreiEmailService.createEmail({
|
||
...req.body,
|
||
customerId,
|
||
});
|
||
await logChange({
|
||
req, action: 'CREATE', resourceType: 'StressfreiEmail',
|
||
resourceId: email.id.toString(),
|
||
label: `Stressfrei-Wechseln Adresse angelegt für Kunde #${customerId}`,
|
||
customerId,
|
||
});
|
||
res.status(201).json({ success: true, data: email } as ApiResponse);
|
||
} catch (error) {
|
||
res.status(400).json({
|
||
success: false,
|
||
error: error instanceof Error ? error.message : 'Fehler beim Erstellen der Stressfrei-Wechseln Adresse',
|
||
} as ApiResponse);
|
||
}
|
||
}
|
||
|
||
export async function updateEmail(req: AuthRequest, res: Response): Promise<void> {
|
||
try {
|
||
const emailId = parseInt(req.params.id);
|
||
if (!(await canAccessStressfreiEmail(req, res, emailId))) return;
|
||
const email = await stressfreiEmailService.updateEmail(emailId, req.body);
|
||
await logChange({
|
||
req, action: 'UPDATE', resourceType: 'StressfreiEmail',
|
||
resourceId: email.id.toString(),
|
||
label: `Stressfrei-Wechseln Adresse aktualisiert`,
|
||
});
|
||
res.json({ success: true, data: email } as ApiResponse);
|
||
} catch (error) {
|
||
res.status(400).json({
|
||
success: false,
|
||
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren der Stressfrei-Wechseln Adresse',
|
||
} as ApiResponse);
|
||
}
|
||
}
|
||
|
||
export async function deleteEmail(req: AuthRequest, res: Response): Promise<void> {
|
||
try {
|
||
const emailId = parseInt(req.params.id);
|
||
if (!(await canAccessStressfreiEmail(req, res, emailId))) return;
|
||
await stressfreiEmailService.deleteEmail(emailId);
|
||
await logChange({
|
||
req, action: 'DELETE', resourceType: 'StressfreiEmail',
|
||
resourceId: emailId.toString(),
|
||
label: `Stressfrei-Wechseln Adresse gelöscht`,
|
||
});
|
||
res.json({ success: true, message: 'Stressfrei-Wechseln Adresse gelöscht' } as ApiResponse);
|
||
} catch (error) {
|
||
res.status(400).json({
|
||
success: false,
|
||
error: error instanceof Error ? error.message : 'Fehler beim Löschen der Stressfrei-Wechseln Adresse',
|
||
} as ApiResponse);
|
||
}
|
||
}
|
||
|
||
export async function syncForwarding(req: AuthRequest, res: Response): Promise<void> {
|
||
try {
|
||
const emailId = parseInt(req.params.id);
|
||
if (!(await canAccessStressfreiEmail(req, res, emailId))) return;
|
||
|
||
const result = await stressfreiEmailService.syncForwardingForEmail(emailId);
|
||
if (!result.success) {
|
||
res.status(400).json({ success: false, error: result.error } as ApiResponse);
|
||
return;
|
||
}
|
||
|
||
const labelParts = [`Weiterleitungen: ${(result.forwardTargets || []).join(', ')}`];
|
||
if (result.passwordReset) labelParts.push('Mailbox-Passwort am Provider neu gesetzt');
|
||
|
||
await logChange({
|
||
req,
|
||
action: 'UPDATE',
|
||
resourceType: 'StressfreiEmail',
|
||
resourceId: emailId.toString(),
|
||
label: `Stressfrei-Sync: ${labelParts.join(' | ')}`,
|
||
});
|
||
|
||
res.json({
|
||
success: true,
|
||
data: {
|
||
forwardTargets: result.forwardTargets,
|
||
customerEmail: result.customerEmail,
|
||
passwordReset: result.passwordReset,
|
||
},
|
||
message: 'Weiterleitungen aktualisiert',
|
||
} as ApiResponse);
|
||
} catch (error) {
|
||
res.status(500).json({
|
||
success: false,
|
||
error: error instanceof Error ? error.message : 'Fehler beim Synchronisieren der Weiterleitungen',
|
||
} as ApiResponse);
|
||
}
|
||
}
|
||
|
||
export async function resetPassword(req: AuthRequest, res: Response): Promise<void> {
|
||
try {
|
||
const emailId = parseInt(req.params.id);
|
||
if (!(await canAccessStressfreiEmail(req, res, emailId))) return;
|
||
const result = await stressfreiEmailService.resetMailboxPassword(emailId);
|
||
if (!result.success) {
|
||
res.status(400).json({
|
||
success: false,
|
||
error: result.error,
|
||
} as ApiResponse);
|
||
return;
|
||
}
|
||
res.json({
|
||
success: true,
|
||
data: { password: result.password },
|
||
message: 'Passwort wurde zurückgesetzt',
|
||
} as ApiResponse);
|
||
} catch (error) {
|
||
res.status(500).json({
|
||
success: false,
|
||
error: error instanceof Error ? error.message : 'Fehler beim Zurücksetzen des Passworts',
|
||
} as ApiResponse);
|
||
}
|
||
}
|