Pentest R120 CRITICAL: IDOR auf Vertragsbaum-Endpunkt schließen

GET /api/contracts?tree=true&customerId=<fremd> gab Portal-Usern
den vollständigen Vertragsbaum beliebiger Fremdkunden zurück
(Name, Kundennummer, Vertragsnummern, Tarife – HTTP 200). Der
tree=true-Zweig returnte früh, bevor die Portal-User-customerIds-
Filterung griff, die für die flache Liste im selben Handler läuft.
Vorbestehender Bug; der includeDeactivated-Toggle machte ihn nur
sichtbarer (auch archivierte Fremdverträge kamen mit). Live vom
Pentester bestätigt, Gegentest ohne den Param = identisches Leck.

Fix: canAccessCustomer(req, res, customerId) vor dem frühen
Return. Prüft eigene ID + vertretene MIT Live-Vollmacht, sendet
selbst die 403. Staff passiert unverändert. Gleiches Muster wie
Pentest 56.3 bei update/delete.

Flacher Listenpfad war nie betroffen: getAllContracts bevorzugt
das serverseitig gesetzte customerIds-Scope gegenüber dem rohen
customerId-Query-Param.

Docs: SECURITY-HARDENING.md § Runde 120 + docs/todo.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 22:31:13 +02:00
co-authored by Claude Opus 4.7
parent d059912c1e
commit 23c530be46
3 changed files with 73 additions and 2 deletions
+12 -2
View File
@@ -10,7 +10,7 @@ import { ApiResponse, AuthRequest } from '../types/index.js';
import { logChange } from '../services/audit.service.js';
import { sanitizeContract, sanitizeContractStrict, sanitizeContracts, sanitizeContractsStrict, stripHtml, sanitizeNotes, validateContractDocumentType, validateOptionalIsoDate, isContractIdentifierField, validateContractIdentifier, validatePortalUsername } from '../utils/sanitize.js';
import { ApiError } from '../utils/apiError.js';
import { canAccessContract } from '../utils/accessControl.js';
import { canAccessContract, canAccessCustomer } from '../utils/accessControl.js';
import { maybeActivateOnDeliveryConfirmation, withContractDocumentLock } from '../services/contractStatusScheduler.service.js';
/**
@@ -78,8 +78,18 @@ export async function getContracts(req: AuthRequest, res: Response): Promise<voi
// Baumstruktur für Kundenansicht
if (tree === 'true' && customerId) {
const customerIdNum = parseInt(customerId as string);
// Pentest R120 (CRITICAL IDOR): Der tree=true-Zweig returnte früh,
// BEVOR die Portal-User-customerIds-Filterung unten für die flache
// Liste griff. Ein Portal-User konnte damit den vollständigen
// Vertragsbaum eines beliebigen Fremdkunden lesen
// (?tree=true&customerId=<fremd>). Ownership-Check nachgezogen
// canAccessCustomer prüft eigene ID + vertretene MIT Live-Vollmacht
// und sendet selbst die 403-Response. Nicht-Portal-User (Staff)
// passieren unverändert.
if (!(await canAccessCustomer(req, res, customerIdNum))) return;
const treeData = await contractService.getContractTreeForCustomer(
parseInt(customerId as string),
customerIdNum,
includeDeactivated === 'true',
);
res.json({ success: true, data: treeData } as ApiResponse);