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:
@@ -50,9 +50,13 @@ function handleError(res: Response, error: unknown, fallback: string) {
|
|||||||
export async function listAll(req: AuthRequest, res: Response): Promise<void> {
|
export async function listAll(req: AuthRequest, res: Response): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// Portal-User: nur eigene + vertretene Kunden MIT Vollmacht.
|
// 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;
|
let customerIds: number[] | undefined;
|
||||||
if (req.user?.isCustomerPortal && req.user.customerId) {
|
if (req.user?.isCustomerPortal) {
|
||||||
customerIds = [req.user.customerId];
|
customerIds = req.user.customerId ? [req.user.customerId] : [];
|
||||||
|
if (req.user.customerId) {
|
||||||
const representedIds: number[] = (req.user as any).representedCustomerIds || [];
|
const representedIds: number[] = (req.user as any).representedCustomerIds || [];
|
||||||
for (const repCustId of representedIds) {
|
for (const repCustId of representedIds) {
|
||||||
if (await authorizationService.hasAuthorization(repCustId, req.user.customerId)) {
|
if (await authorizationService.hasAuthorization(repCustId, req.user.customerId)) {
|
||||||
@@ -60,6 +64,7 @@ export async function listAll(req: AuthRequest, res: Response): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const page = parseInt((req.query.page as string) || '1') || 1;
|
const page = parseInt((req.query.page as string) || '1') || 1;
|
||||||
const limit = Math.min(parseInt((req.query.limit as string) || '50') || 50, 200);
|
const limit = Math.min(parseInt((req.query.limit as string) || '50') || 50, 200);
|
||||||
|
|||||||
@@ -107,6 +107,9 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
|
|||||||
`customerIds` kommt aus dem JWT (nicht aus Query/Body → nicht manipulierbar).
|
`customerIds` kommt aus dem JWT (nicht aus Query/Body → nicht manipulierbar).
|
||||||
Für Portal wird `receiptPath` aus der Response entfernt (Belege bleiben
|
Für Portal wird `receiptPath` aus der Response entfernt (Belege bleiben
|
||||||
staff-only). Verifiziert (Staff alle, Portal nur eigene).
|
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)
|
- [x] **🚫 Gutschrift nur mit Empfängeradresse (Rechnung > Liefer)** (2026-08-12)
|
||||||
- Beim Klick auf „Gutschrift anlegen" wird geprüft, ob der Vertrag eine
|
- Beim Klick auf „Gutschrift anlegen" wird geprüft, ob der Vertrag eine
|
||||||
Empfängeradresse hat: **Rechnungsadresse hat Vorrang, sonst Lieferadresse**.
|
Empfängeradresse hat: **Rechnungsadresse hat Vorrang, sonst Lieferadresse**.
|
||||||
|
|||||||
Reference in New Issue
Block a user