Security-Hardening Runde 14: Factory-Reset, Settings-Whitelist, Prisma-Leak, XSS-Strip

Pentest Runde 11:

C2 KRITISCH – Factory Reset ohne Bestätigung:
Eingeloggter Admin konnte mit leerem oder beliebigem Body die DB
plätten (3× in einer Pentest-Session passiert). Server erzwingt jetzt
confirm:"FACTORY-RESET-BESTAETIGT" als String. Frontend-API sendet
den Wert automatisch mit.

M1 – Settings Mass Assignment:
PUT /api/settings akzeptierte beliebige Keys (superAdminEmail,
debugMode, allowedOrigins). Neue Whitelist ALLOWED_SETTING_KEYS in
appSetting.service.ts; updateSetting + updateSettings prüfen jeden
Key, unbekannte → 400.

M3 – Prisma-Error-Leak:
Statt 30+ Controller einzeln zu fixen, globaler res.json()-Wrapper
unter /api: error/details-Strings werden durch Pattern-Filter
geschickt, der ORM-/Stack-Trace-Muster zu "Operation fehlgeschlagen"
ersetzt. Original bleibt im Server-Log.

M2 – Stored XSS in Customer/User-Strings:
Neuer stripHtml()-Helper. pickCustomerUpdate/Create + pickUserUpdate/
Create rufen ihn auf jeden String-Wert. Defense-in-Depth gegen PDF/
E-Mail-Template-XSS-Vektoren – React-Frontend ist eh auto-escaped.

Live-verifiziert:
- factory-reset {} / {confirm:true} / {confirm:false} → 400, DB ok
- PUT /settings {superAdminEmail,...} → 400 + Keys aufgezählt;
  PUT /settings {customerSupportTicketsEnabled:"true"} → 200
- PUT /users/99999 → "Operation fehlgeschlagen" (vorher Prisma-Stack)
- PUT /customers/3 {companyName:"<script>...</script>EvilCorp"} →
  gespeichert als "EvilCorp"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-18 05:23:12 +02:00
parent ef238b0145
commit d545790a69
7 changed files with 168 additions and 8 deletions
@@ -41,6 +41,15 @@ export async function updateSetting(req: AuthRequest, res: Response): Promise<vo
return;
}
// Whitelist-Check (Pentest Runde 11, M1)
if (!appSettingService.isAllowedSettingKey(key)) {
res.status(400).json({
success: false,
error: `Unbekannter Setting-Key: ${key}`,
} as ApiResponse);
return;
}
// Vorherigen Stand laden für Audit
const before = await prisma.appSetting.findUnique({ where: { key } });
const oldValue = before?.value ?? '-';
@@ -78,6 +87,18 @@ export async function updateSettings(req: AuthRequest, res: Response): Promise<v
return;
}
// Whitelist-Check für jeden Key (Pentest Runde 11, M1: Mass Assignment)
const unknownKeys = Object.keys(settings).filter(
(k) => !appSettingService.isAllowedSettingKey(k),
);
if (unknownKeys.length > 0) {
res.status(400).json({
success: false,
error: `Unbekannte Setting-Keys: ${unknownKeys.join(', ')}`,
} as ApiResponse);
return;
}
// Vorherige Werte laden für Audit
const changes: Record<string, { von: unknown; nach: unknown }> = {};
for (const [key, value] of Object.entries(settings)) {