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:
@@ -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)) {
|
||||
|
||||
@@ -176,6 +176,22 @@ export async function uploadBackup(req: Request, res: Response) {
|
||||
*/
|
||||
export async function factoryReset(req: Request, res: Response) {
|
||||
try {
|
||||
// Bestätigung erforderlich: client MUSS explizit
|
||||
// `confirm: "FACTORY-RESET-BESTAETIGT"` schicken. Ohne diesen Schritt
|
||||
// konnte ein eingeloggter Admin die komplette DB mit einem einfachen
|
||||
// POST plätten (Pentest Runde 11 (2026-05-18) – C2 KRITISCH:
|
||||
// 3× DB-Plättung in einer Session). Body-Wert ist absichtlich ein
|
||||
// unique String und kein boolean, damit kein Auto-JSON-Tooling /
|
||||
// Replay-Angriff aus Versehen triggern kann.
|
||||
const confirm = (req.body && req.body.confirm) ? String(req.body.confirm) : '';
|
||||
if (confirm !== 'FACTORY-RESET-BESTAETIGT') {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Bestätigung fehlt. Body muss { "confirm": "FACTORY-RESET-BESTAETIGT" } enthalten.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await backupService.factoryReset();
|
||||
|
||||
if (result.success) {
|
||||
@@ -190,6 +206,7 @@ export async function factoryReset(req: Request, res: Response) {
|
||||
res.status(500).json({ error: 'Werkseinstellungen fehlgeschlagen', details: result.error });
|
||||
}
|
||||
} catch (error: any) {
|
||||
res.status(500).json({ error: 'Fehler bei Werkseinstellungen', details: error.message });
|
||||
res.status(500).json({ error: 'Fehler bei Werkseinstellungen' });
|
||||
console.error('factoryReset error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user