diff --git a/backend/src/controllers/backup.controller.ts b/backend/src/controllers/backup.controller.ts index 5c40e85f..0ca404d9 100644 --- a/backend/src/controllers/backup.controller.ts +++ b/backend/src/controllers/backup.controller.ts @@ -184,6 +184,19 @@ export async function restoreBackup(req: Request, res: Response) { return res.status(400).json({ error: 'Ungültiger Backup-Name' }); } + // Pflicht-Confirm im Body, gleiche Defensive wie factoryReset. + // Pentest 2026-05-19 (KRITISCH): leerer POST-Body löste vorher + // sofort den destruktiven Restore aus – ein versehentlicher + // Re-Fire (Browser-Tab, CSRF auf eingeloggten Admin, doppelter + // Klick) konnte die DB ungewollt überschreiben. Der String ist + // bewusst ein unique Magic-Value, kein Boolean. + const confirm = (req.body && req.body.confirm) ? String(req.body.confirm) : ''; + if (confirm !== 'RESTORE-BESTAETIGT') { + return res.status(400).json({ + error: 'Bestätigung fehlt. Body muss { "confirm": "RESTORE-BESTAETIGT" } enthalten.', + }); + } + const capture = startLogCapture(); try { const result = await backupService.restoreBackup(name); diff --git a/docs/todo.md b/docs/todo.md index efeaab22..8a8b45a5 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -120,6 +120,22 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung - **Live-verifiziert**: 4867 Datensätze + 1 Datei in 13.2s wiederhergestellt, Log-Modal zeigt den vollständigen Verlauf. +- [x] **🚨 Pentest 2026-05-20 KRITISCH: Backup-Restore ohne Confirm-Body** + - `POST /api/settings/backup/:name/restore` startete bei leerem + Body sofort den destruktiven Restore. Im Unterschied zu + `/factory-reset` fehlte der Magic-String-Confirm-Check. Risiko: + versehentlicher Re-Fire (Doppelklick, Browser-Replay, eingeloggter + Admin auf bösartiger Drittseite) überschrieb stillschweigend die + komplette DB. + - Fix: gleicher Defensive-Pattern wie factoryReset – Body muss + `{ "confirm": "RESTORE-BESTAETIGT" }` enthalten, sonst 400. + Frontend-Client schickt den String beim Klick im Bestätigungs- + Dialog automatisch (kein UX-Change für den User). + - **Live-verifiziert** auf dev: + - leerer Body → 400 "Bestätigung fehlt" + - `{"confirm":"ja"}` → 400 (wrong) + - `{"confirm":"RESTORE-BESTAETIGT"}` → 200, Restore lief + - [x] **🛡️ XSS-Sanitization für Plain-Text-AppSettings (Pentest MEDIUM)** - `companyName` (und weitere Plain-Text-Keys wie `defaultEmailDomain`, `monitoringAlertEmail`, Schwellenwerte) konnten via PUT diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index e2f7c4f2..ae188fad 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -983,7 +983,13 @@ export const backupApi = { return res.data; }, restore: async (name: string) => { - const res = await api.post>(`/settings/backup/${name}/restore`); + // Server erzwingt confirm-Body als Schutz gegen versehentliche + // Datenüberschreibung (Pentest 2026-05-19 KRITISCH, Analog zu + // factoryReset). Der Confirm-String muss exakt dieser Wert sein. + const res = await api.post>( + `/settings/backup/${name}/restore`, + { confirm: 'RESTORE-BESTAETIGT' }, + ); return res.data; }, delete: async (name: string) => {