Security-Hardening Runde 13: Live-Vollmacht-Konsistenz + embedded DTOs
Pentest Runde 10: MEDIUM – Stale Token nach Vollmacht-Widerruf: Selbst ein frischer Portal-Login lieferte JWT mit representedCustomer- Ids/representedCustomers, obwohl die Vollmacht widerrufen war. Live- Check beim Datenzugriff fing das ab (403), aber die UI zeigte weiter „kann vertreten". customerLogin und getCustomerPortalUser (= /me + Refresh) filtern representingFor jetzt zusätzlich über getAuthorizedCustomerIds() – nur Beziehungen mit isGranted=true landen im Token. MEDIUM – DTO-Leak in embedded Objekten: GET /customers/:id lieferte contracts[] mit commission/notes/ portalPasswordEncrypted/nextReviewDate; embedded customer in /contracts/:id zeigte notes. sanitizeCustomer(Strict) ruft jetzt sanitizeContract(Strict) auf jedes Element von contracts[] auf; `notes` ist als PORTAL_HIDDEN_CUSTOMER_FIELDS aufgenommen. LOW – /tasks?customerId=X gibt 200 mit leerem Array statt 403: Konsistenz-Fix: wenn Portal-User explizit nach customerId filtert, die er nicht vertreten darf → 403. Live-verifiziert: - Customer 1 vertritt 2+3 (Vollmachten widerrufen) → JWT representedCustomerIds=[], /me dito - Portal /customers/1.contracts[0]: keine Leaks; Admin sieht weiter commission/notes; portalPasswordEncrypted generell weg - Portal /tasks?customerId=2 → 403; /tasks?customerId=1 → 200 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import { JwtPayload } from '../types/index.js';
|
||||
import { encrypt, decrypt } from '../utils/encryption.js';
|
||||
import { sendEmail, SmtpCredentials } from './smtpService.js';
|
||||
import { getSystemEmailCredentials } from './emailProvider/emailProviderService.js';
|
||||
import { getAuthorizedCustomerIds } from './authorization.service.js';
|
||||
|
||||
// Token-Lifetimes
|
||||
// - Access-Token: kurzlebig, nur im Browser-Memory → XSS klaut max. 15 min
|
||||
@@ -216,10 +217,17 @@ export async function customerLogin(email: string, password: string) {
|
||||
});
|
||||
}
|
||||
|
||||
// IDs der Kunden sammeln, die dieser Kunde vertreten kann
|
||||
const representedCustomerIds = customer.representingFor.map(
|
||||
(rep) => rep.customer.id
|
||||
// IDs der Kunden sammeln, die dieser Kunde vertreten kann –
|
||||
// GEFILTERT auf aktive Vollmacht (isGranted: true). Ohne diesen Filter
|
||||
// hätte das frische JWT nach Vollmacht-Widerruf weiterhin die alte
|
||||
// representedCustomerIds-Liste; die UI würde dem Vertreter noch
|
||||
// anzeigen, dass er vertreten kann, obwohl der Live-Check beim
|
||||
// Datenzugriff dann 403 wirft. Pentest Runde 10 (2026-05-17), MEDIUM.
|
||||
const grantedCustomerIds = new Set(await getAuthorizedCustomerIds(customer.id));
|
||||
const grantedRepresentingFor = customer.representingFor.filter((rep) =>
|
||||
grantedCustomerIds.has(rep.customer.id),
|
||||
);
|
||||
const representedCustomerIds = grantedRepresentingFor.map((rep) => rep.customer.id);
|
||||
|
||||
// Kundenportal-Berechtigungen (eingeschränkt)
|
||||
const customerPermissions = [
|
||||
@@ -251,7 +259,7 @@ export async function customerLogin(email: string, password: string) {
|
||||
customerId: customer.id,
|
||||
isCustomerPortal: true,
|
||||
mustChangePassword,
|
||||
representedCustomers: customer.representingFor.map((rep) => ({
|
||||
representedCustomers: grantedRepresentingFor.map((rep) => ({
|
||||
id: rep.customer.id,
|
||||
customerNumber: rep.customer.customerNumber,
|
||||
firstName: rep.customer.firstName,
|
||||
@@ -538,6 +546,13 @@ export async function getCustomerPortalUser(customerId: number) {
|
||||
'customers:read',
|
||||
];
|
||||
|
||||
// Selbe Live-Vollmacht-Filterung wie in customerLogin (Pentest Runde 10):
|
||||
// ohne sie zeigt /me dem Vertreter weiterhin widerrufene Beziehungen.
|
||||
const grantedCustomerIds = new Set(await getAuthorizedCustomerIds(customer.id));
|
||||
const grantedRepresentingFor = customer.representingFor.filter((rep) =>
|
||||
grantedCustomerIds.has(rep.customer.id),
|
||||
);
|
||||
|
||||
return {
|
||||
id: customer.id,
|
||||
email: customer.portalEmail,
|
||||
@@ -547,7 +562,7 @@ export async function getCustomerPortalUser(customerId: number) {
|
||||
customerId: customer.id,
|
||||
permissions: customerPermissions,
|
||||
isCustomerPortal: true,
|
||||
representedCustomers: customer.representingFor.map((rep) => ({
|
||||
representedCustomers: grantedRepresentingFor.map((rep) => ({
|
||||
id: rep.customer.id,
|
||||
customerNumber: rep.customer.customerNumber,
|
||||
firstName: rep.customer.firstName,
|
||||
|
||||
Reference in New Issue
Block a user