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:
@@ -29,11 +29,24 @@ export async function getCustomers(req: AuthRequest, res: Response): Promise<voi
|
||||
page: page ? parseInt(page as string) : undefined,
|
||||
limit: limit ? parseInt(limit as string) : undefined,
|
||||
});
|
||||
let customers = result.customers as any[];
|
||||
|
||||
// Portal-Kunden: Liste auf eigenen + vertretene Kunden einschränken.
|
||||
// Ohne diesen Filter würde der List-Endpoint die komplette Kundendatenbank
|
||||
// an einen einzelnen Portal-Account preisgeben.
|
||||
if (req.user?.isCustomerPortal) {
|
||||
const allowedIds = new Set<number>();
|
||||
if (req.user.customerId) allowedIds.add(req.user.customerId);
|
||||
const represented = (req.user as any).representedCustomerIds || [];
|
||||
for (const id of represented) allowedIds.add(id);
|
||||
customers = customers.filter((c) => allowedIds.has(c.id));
|
||||
}
|
||||
|
||||
// Portal-Kunden oder andere Rollen sehen kein portalPasswordEncrypted
|
||||
const canSeePasswords = req.user?.permissions?.includes('customers:update') ?? false;
|
||||
const sanitized = canSeePasswords
|
||||
? sanitizeCustomers(result.customers as any)
|
||||
: (result.customers as any[]).map((c) => sanitizeCustomerStrict(c)).filter(Boolean);
|
||||
? sanitizeCustomers(customers)
|
||||
: customers.map((c) => sanitizeCustomerStrict(c)).filter(Boolean);
|
||||
res.json({ success: true, data: sanitized, pagination: result.pagination } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
|
||||
Reference in New Issue
Block a user