diff --git a/backend/src/controllers/user.controller.ts b/backend/src/controllers/user.controller.ts index c9732fb0..611f809e 100644 --- a/backend/src/controllers/user.controller.ts +++ b/backend/src/controllers/user.controller.ts @@ -655,21 +655,31 @@ function antworteAufRollenFehler(res: Response, error: unknown, fallback: string return; } - // Datenbankfehler NICHT durchreichen. Prisma haengt den vollstaendigen - // Aufruf samt Dateipfad an die Meldung - das ging bisher wortwoertlich an - // den Client. Nach aussen die allgemeine Auskunft, die Einzelheiten ins - // Serverprotokoll. - const name = error instanceof Error ? error.name : ''; - if (name.startsWith('Prisma')) { - console.error(`[${fallback}]`, error); - res.status(400).json({ success: false, error: fallback } as ApiResponse); + // Ab hier gilt: Nur was wir SELBST formuliert haben, geht nach draussen. + // + // Vorher wurde jede `error.message` durchgereicht. Damit landete erst der + // vollstaendige Prisma-Aufruf samt Serverpfad beim Client, und danach der + // Wortlaut eines TypeError ("object is not iterable…") bei + // `{"roleIds":{}}` (Pentest R190-01 und R192-01). Beide Male dieselbe + // Ursache: eine Fehlermeldung, die fuer Entwickler geschrieben ist, an + // einen Empfaenger, fuer den sie nicht gedacht war. + // + // Die Unterscheidung ist die Fehlerklasse, keine Heuristik auf dem Text: + // Ein blankes `Error` werfen wir absichtlich und mit einer Meldung fuer + // Menschen ("Der letzte Admin kann nicht…"). TypeError, RangeError und + // Verwandte sind Programmierfehler, Prisma-Fehler kommen von aussen - + // beides sagt dem Aufrufer nichts Nuetzliches und dem Angreifer zu viel. + const istAbsichtlich = error instanceof Error && error.constructor === Error; + if (istAbsichtlich) { + res.status(400).json({ success: false, error: error.message } as ApiResponse); return; } - res.status(400).json({ - success: false, - error: error instanceof Error ? error.message : fallback, - } as ApiResponse); + // Unerwartet: Das ist ein Serverfehler, kein Eingabefehler - und ein 400 + // waere hier die naechste Meldung, die sich falsch ausgibt. Einzelheiten + // ins Protokoll, damit sie nicht verlorengehen. + console.error(`[${fallback}] unerwarteter Fehler:`, error); + res.status(500).json({ success: false, error: fallback } as ApiResponse); } export async function createRole(req: AuthRequest, res: Response): Promise { diff --git a/backend/src/services/rechte.service.ts b/backend/src/services/rechte.service.ts index b552e405..9fbc8d5f 100644 --- a/backend/src/services/rechte.service.ts +++ b/backend/src/services/rechte.service.ts @@ -216,6 +216,12 @@ export function rechteDerHaken(haken: { * falsch als Eingabepruefung - zwei verschiedene Aufgaben. */ export async function normalisiereRollenIds(roleIds: number[]): Promise { + // Erst pruefen, dass es ueberhaupt eine Liste ist. Ohne das lief ein + // `{"roleIds":{}}` in einen TypeError beim Aufspreizen, und dessen + // Wortlaut ging an den Client (Pentest R192-01). + if (!Array.isArray(roleIds)) { + throw new UngueltigeEingabeError('roleIds muss eine Liste sein'); + } const eindeutig = [...new Set(roleIds)]; if (eindeutig.length === 0) return []; if (!eindeutig.every((id) => Number.isInteger(id) && id >= 1)) { diff --git a/docs/todo.md b/docs/todo.md index 45e6651b..5cf5bbcf 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -100,6 +100,35 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung + +- [x] **🤐 R192-01: Fehlermeldungen, die für Entwickler geschrieben sind** (2026-09-09) + - Befund der Pentesterin: `PUT /users/:id {"roleIds":{}}` gab 400 mit dem + rohen JS-Fehler `object is not iterable (cannot read property + Symbol(Symbol.iterator))`. Kein Wipe, keine Eskalation — dieselbe Familie + wie der Prisma-Pfad-Leak aus R190-01. + - Zwei Ursachen, beide behoben: + 1. `normalisiereRollenIds` spreizte `roleIds` ohne `Array.isArray`-Guard. + 2. Die Fehlerabbildung reichte **jede** `error.message` durch. + - Die Unterscheidung läuft jetzt über die **Fehlerklasse**, nicht über eine + Heuristik auf dem Text: Ein blankes `Error` werfen wir absichtlich und mit + einer Meldung für Menschen („Der letzte Admin kann nicht…"). `TypeError` + und Verwandte sind Programmierfehler, Prisma-Fehler kommen von außen — + beides sagt dem Aufrufer nichts Nützliches und einem Angreifer zu viel. + Unerwartetes wird jetzt **500** statt 400: Ein Eingabefehler-Code wäre die + nächste Meldung, die sich falsch ausgibt. + - Gegengeprüft, dass mit dem Leck nicht auch das Nützliche wegfällt: + „Rolle kann nicht gelöscht werden, da sie 1 Benutzern zugewiesen ist" → 400, + Systemrollen-Sperre → 403 mit Klartext, Eskalation → 403 mit den konkret + fehlenden Rechten. `{}`, `"abc"`, `5`, `true` als `roleIds` → 400 + „roleIds muss eine Liste sein", Zustand unverändert. R190-01/R191-01 + grün. + - **⚠️ Offen, größer als der Befund:** Das Muster + `error instanceof Error ? error.message` steht **124-mal in 26 + Controller-Dateien**. Behoben ist es nur im Benutzer-/Rollenpfad. Das ist + dieselbe Lage wie bei R188 (181 ungeprüfte IDs) und gehört genauso + zentral gelöst statt 124-mal einzeln — eigene Runde. + - Dateien: `src/services/rechte.service.ts`, `src/controllers/user.controller.ts` + - [x] **🔗 R191-01: Ein PUT, eine Klammer** (2026-09-09) - Befund der Pentesterin nach dem R190-01-Fix: Nur der Rollentausch lag in der Transaktion, die drei Haken (DSGVO/Developer/Audit-Betrieb) liefen