Pentest KRITISCH: Backup-Restore braucht 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, sodass ein versehentlicher
Re-Fire (Doppelklick, Browser-Tab-Replay, eingeloggter Admin auf
bösartiger Drittseite) die komplette DB stillschweigend
überschreiben konnte.

Fix: gleicher Defensive-Pattern wie factoryReset – Body muss
{ "confirm": "RESTORE-BESTAETIGT" } enthalten, sonst 400. Der
Magic-String ist absichtlich ein einzigartiges Token (kein Boolean),
damit kein Auto-JSON-Tooling/Replay aus Versehen triggern kann.

Frontend-API-Client setzt das Token im Body automatisch – der
existierende Bestätigungs-Dialog im UI bleibt UX-mäßig unverändert.

Live-verifiziert:
- leerer Body → 400
- { confirm: "ja" } → 400
- { confirm: "RESTORE-BESTAETIGT" } → 200, Restore läuft

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-20 01:05:00 +02:00
parent b3a6620da6
commit bf7afdd9a6
3 changed files with 36 additions and 1 deletions
@@ -184,6 +184,19 @@ export async function restoreBackup(req: Request, res: Response) {
return res.status(400).json({ error: 'Ungültiger Backup-Name' }); 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(); const capture = startLogCapture();
try { try {
const result = await backupService.restoreBackup(name); const result = await backupService.restoreBackup(name);
+16
View File
@@ -120,6 +120,22 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
- **Live-verifiziert**: 4867 Datensätze + 1 Datei in 13.2s - **Live-verifiziert**: 4867 Datensätze + 1 Datei in 13.2s
wiederhergestellt, Log-Modal zeigt den vollständigen Verlauf. 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)** - [x] **🛡️ XSS-Sanitization für Plain-Text-AppSettings (Pentest MEDIUM)**
- `companyName` (und weitere Plain-Text-Keys wie `defaultEmailDomain`, - `companyName` (und weitere Plain-Text-Keys wie `defaultEmailDomain`,
`monitoringAlertEmail`, Schwellenwerte) konnten via PUT `monitoringAlertEmail`, Schwellenwerte) konnten via PUT
+7 -1
View File
@@ -983,7 +983,13 @@ export const backupApi = {
return res.data; return res.data;
}, },
restore: async (name: string) => { restore: async (name: string) => {
const res = await api.post<ApiResponse<{ restoredRecords: number; restoredFiles: number }>>(`/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<ApiResponse<{ restoredRecords: number; restoredFiles: number }>>(
`/settings/backup/${name}/restore`,
{ confirm: 'RESTORE-BESTAETIGT' },
);
return res.data; return res.data;
}, },
delete: async (name: string) => { delete: async (name: string) => {