From 4c5548ea3ccb10ef84a44f49df70c9e4271bb708 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Wed, 12 Aug 2026 11:18:46 +0200 Subject: [PATCH] =?UTF-8?q?Beleg=C3=BCbersicht:=20fail-closed=20Portal-Sco?= =?UTF-8?q?ping=20(R144)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pentester R144 (fail-closed, kein Finding): listAll scopte ueber 'if (isCustomerPortal && customerId)'. Fiele customerId bei einem Portal-Token mal falsy aus, rutschte er in den Staff-Zweig (alle Belege). Jetzt: Portal-Token wird IMMER gescoped; ohne customerId -> leere Menge (customerIds=[]) statt undefined/Staff. In:[] kann nie matchen. Aktuell nicht erreichbar (Portal-Token traegt immer customerId), aber robuster. Verifiziert: customerIds=[] -> 0 Belege. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/controllers/creditNote.controller.ts | 17 +++++++++++------ docs/todo.md | 3 +++ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/backend/src/controllers/creditNote.controller.ts b/backend/src/controllers/creditNote.controller.ts index e60681e1..39b8ac8c 100644 --- a/backend/src/controllers/creditNote.controller.ts +++ b/backend/src/controllers/creditNote.controller.ts @@ -50,13 +50,18 @@ function handleError(res: Response, error: unknown, fallback: string) { export async function listAll(req: AuthRequest, res: Response): Promise { try { // Portal-User: nur eigene + vertretene Kunden MIT Vollmacht. + // Fail-closed (Pentest R144): Ein Portal-Token wird IMMER gescoped. Fehlt + // wider Erwarten die customerId (null/0), ergibt das eine LEERE Menge – + // nicht den Staff-Zweig (der alle Belege zeigen würde). let customerIds: number[] | undefined; - if (req.user?.isCustomerPortal && req.user.customerId) { - customerIds = [req.user.customerId]; - const representedIds: number[] = (req.user as any).representedCustomerIds || []; - for (const repCustId of representedIds) { - if (await authorizationService.hasAuthorization(repCustId, req.user.customerId)) { - customerIds.push(repCustId); + if (req.user?.isCustomerPortal) { + customerIds = req.user.customerId ? [req.user.customerId] : []; + if (req.user.customerId) { + const representedIds: number[] = (req.user as any).representedCustomerIds || []; + for (const repCustId of representedIds) { + if (await authorizationService.hasAuthorization(repCustId, req.user.customerId)) { + customerIds.push(repCustId); + } } } } diff --git a/docs/todo.md b/docs/todo.md index 11229897..8cde4d89 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -107,6 +107,9 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung `customerIds` kommt aus dem JWT (nicht aus Query/Body → nicht manipulierbar). Für Portal wird `receiptPath` aus der Response entfernt (Belege bleiben staff-only). Verifiziert (Staff alle, Portal nur eigene). + - **Fail-closed (Pentest R144):** Ein Portal-Token wird jetzt IMMER gescoped; + fehlt wider Erwarten die `customerId` (null/0), ergibt das eine **leere + Menge** statt des Staff-Zweigs. Verifiziert (`customerIds=[]` → 0 Belege). - [x] **🚫 Gutschrift nur mit Empfängeradresse (Rechnung > Liefer)** (2026-08-12) - Beim Klick auf „Gutschrift anlegen" wird geprüft, ob der Vertrag eine Empfängeradresse hat: **Rechnungsadresse hat Vorrang, sonst Lieferadresse**.