Pentest 2026-05-24 Pen-31-Befunde (2x MEDIUM)

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>
This commit is contained in:
2026-05-24 15:38:16 +02:00
parent 897abc7b21
commit aa0900410b
4 changed files with 103 additions and 20 deletions
@@ -2,11 +2,17 @@ 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 { canAccessStressfreiEmail } from '../utils/accessControl.js';
import { canAccessCustomer, canAccessStressfreiEmail } from '../utils/accessControl.js';
export async function getEmailsByCustomer(req: Request, res: Response): Promise<void> {
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);