Security-Hardening Runde 6: Customer-Liste-Leak + XFF-Bypass + Vollmacht-Validation

Tiefer Live-Pentest deckte 3 weitere Schwachstellen:

🚨 CRITICAL: GET /api/customers leakte komplette Kundendatenbank
- Stage-4 hatte canAccessCustomer auf den Single-Endpoint angewendet,
  der List-Endpoint hatte nur den Daten-Sanitizer (filtert Passwort-Hashes)
  aber keinen Portal-Filter. Folge: Portal-Kunde sah ALLE Kunden mit Namen,
  E-Mails, customerNumber etc. – DSGVO-relevant.
- Fix: getCustomers filtert für Portal-User auf eigene + vertretene IDs.

🚨 HIGH: Rate-Limit-Bypass via X-Forwarded-For
- `trust proxy = 1` hat jedem XFF-Wert vertraut. 12+ Logins mit
  rotierender XFF-IP gingen ohne 429 durch.
- Fix: `trust proxy = 'loopback'` – XFF nur noch von 127.0.0.1 / ::1
  akzeptiert (= lokaler Reverse-Proxy).
- Plus: LISTEN_ADDR-Default 127.0.0.1 in Production, damit das Backend
  nicht von außen direkt ansprechbar ist.

🛡 MEDIUM: Self-Grant + Existence-Disclosure in toggleMyAuthorization
- Portal-User konnte:
  a) sich selbst Vollmacht erteilen (customerId=representativeId=1)
  b) Authorization-Records für nicht-existierende customerIds anlegen
     (scheitert erst am DB-Constraint mit vollem Prisma-Stack-Leak)
  c) Customer-IDs durch 404-vs-403-Differenzen enumerieren.
- Fix: Self-Grant 400. Existenz + aktive CustomerRepresentative-Beziehung
  in einem Query – Non-Existent / Non-Related geben identisch 403.
  Prisma-Error-Stacks generisch ersetzt.

Live-verifiziert: Customer-Liste filtert, Self-Grant 400, Existence-Probing
dicht.

Geprüft + sauber (Runde 6, kein Bug):
- Prototype Pollution Login-Body
- HTTP-Method-Override-Header
- Path-Traversal Backup-Name (Regex blockt)
- Developer-Routes existieren nicht
- Email-Endpoints mit fremder StressfreiEmail-ID → 403
- /api/customers/:id GET liefert 403 statt 404 (kein Leak)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-26 20:14:20 +02:00
parent 35745ce3bb
commit 0c0cecdbbd
4 changed files with 95 additions and 17 deletions
+23 -9
View File
@@ -970,12 +970,27 @@ export async function toggleMyAuthorization(req: AuthRequest, res: Response) {
const representativeId = parseInt(req.params.representativeId);
const { grant } = req.body;
// Vertreter-Name laden
const representative = await prisma.customer.findUnique({
where: { id: representativeId },
select: { firstName: true, lastName: true },
// Validierungen:
// 1) Self-Grant verhindern (sinnlos und schafft Datenmüll).
if (representativeId === user.customerId) {
return res.status(400).json({ success: false, error: 'Kein Self-Grant möglich' });
}
// 2) Existenz + aktives Vertreter-Verhältnis in EINEM Lookup prüfen.
// Beide Fälle (representative existiert nicht / keine aktive Beziehung)
// geben identisch 403, damit ein Angreifer keine Customer-IDs aus der
// DB enumerieren kann (kein 404-vs-403-Disclosure).
const relation = await prisma.customerRepresentative.findFirst({
where: { customerId: user.customerId, representativeId, isActive: true },
include: { representative: { select: { firstName: true, lastName: true } } },
});
const repName = representative ? `${representative.firstName} ${representative.lastName}` : `#${representativeId}`;
if (!relation) {
return res.status(403).json({
success: false,
error: 'Kein Vertreter-Verhältnis Vollmacht nicht erlaubt',
});
}
const repName = `${relation.representative.firstName} ${relation.representative.lastName}`;
let auth;
if (grant) {
@@ -991,10 +1006,9 @@ export async function toggleMyAuthorization(req: AuthRequest, res: Response) {
res.json({ success: true, data: auth });
} catch (error) {
console.error('Fehler beim Ändern der Vollmacht:', error);
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Ändern',
});
// Generische Fehlermeldung Prisma-Errors enthalten Pfad/Schema und
// sollten nicht an Endkunden geleakt werden.
res.status(400).json({ success: false, error: 'Vollmacht konnte nicht aktualisiert werden' });
}
}