Belegübersicht: fail-closed Portal-Scoping (R144)

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) <noreply@anthropic.com>
This commit is contained in:
2026-08-12 11:18:46 +02:00
co-authored by Claude Opus 4.8
parent 48e65be91c
commit 4c5548ea3c
2 changed files with 14 additions and 6 deletions
@@ -50,13 +50,18 @@ function handleError(res: Response, error: unknown, fallback: string) {
export async function listAll(req: AuthRequest, res: Response): Promise<void> {
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);
}
}
}
}
+3
View File
@@ -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**.