Security-Hardening Runde 10: Pentest Runde 6 (8 Findings + struktureller Audit-Sweep)
KRITISCH: - emails/:id/thread bekommt canAccessCachedEmail - customers/:customerId/representatives/search bekommt canAccessCustomer (Buchstaben-Brute-Force konnte sonst die Kunden-DB enumerieren) HOCH: - birthdays/upcoming: Portal-User → 403 (Name/E-Mail/Telefon/Geb-Datum aller Kunden leakte) - contracts/:id/history (GET/POST/PUT/DELETE) bekommt canAccessContract - mailbox-accounts / unread-count / contracts/:id/emails/folder-counts bekommen canAccessCustomer bzw. canAccessContract - Vertreter-Vollmacht-Check ist jetzt live: neuer Helper getPortalAllowedCustomerIds() in accessControl.ts ruft hasAuthorization() für jedes vertretene Customer ab. Eingesetzt in getTasks/createSupportTicket/createCustomerReply/getAllTasks/ getTaskStats und updateCustomerConsent. Widerrufene Vollmachten haben jetzt SOFORT keinen Zugriff mehr (vorher: bis JWT abläuft). MITTEL: - confirmPasswordReset speichert portalPasswordEncrypted nicht mehr beim Self-Service-Reset (war nur für Admin-OTPs gedacht); + portalPasswordMustChange=false explizit - getCustomers pagination total reflektiert jetzt nur erlaubte IDs (über DB-Filter in customerService.getAllCustomers) Audit-Sweep (defense in depth, falls Rolle versehentlich Update- Permissions bekommt): - 16 cachedEmail-Operationen (markAsRead, toggleStar, assign/unassign, save-as-pdf/invoice/contract-document, save-to, attachment-targets, trash-ops) - 4 contract-Operationen (createFollowUp, createRenewal, snoozeContract, removeContractMeter) - 12 sub-CRUD-Operationen (address/bankcard/document/meter update+delete, meter-reading add/update/delete/transfer) - 2 representative-Operationen (add/remove) Live-verifiziert: Portal-Customer-3 auf alle fremden IDs → 403, Admin sieht alles, eigene Ressourcen weiterhin 200, Customer 1 mit widerrufener Vollmacht für Customer 3 → 0 fremde Verträge in der Response. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import * as customerService from '../services/customer.service.js';
|
||||
import * as appSettingService from '../services/appSetting.service.js';
|
||||
import { logChange } from '../services/audit.service.js';
|
||||
import { ApiResponse, AuthRequest } from '../types/index.js';
|
||||
import { canAccessContract, getPortalAllowedCustomerIds } from '../utils/accessControl.js';
|
||||
|
||||
// ==================== ALL TASKS (Dashboard & Task List) ====================
|
||||
|
||||
@@ -12,12 +13,13 @@ export async function getAllTasks(req: AuthRequest, res: Response): Promise<void
|
||||
try {
|
||||
const { status, customerId } = req.query;
|
||||
|
||||
// Für Kundenportal: Filter auf erlaubte Kunden
|
||||
// Für Kundenportal: Filter auf erlaubte Kunden (mit Live-Vollmacht-Check)
|
||||
const allowedIds = await getPortalAllowedCustomerIds(req);
|
||||
let customerPortalCustomerIds: number[] | undefined;
|
||||
let customerPortalEmails: string[] | undefined;
|
||||
|
||||
if (req.user?.isCustomerPortal && req.user.customerId) {
|
||||
customerPortalCustomerIds = [req.user.customerId, ...(req.user.representedCustomerIds || [])];
|
||||
if (allowedIds) {
|
||||
customerPortalCustomerIds = allowedIds;
|
||||
const customers = await customerService.getCustomersByIds(customerPortalCustomerIds);
|
||||
customerPortalEmails = customers
|
||||
.map((c: { id: number; portalEmail: string | null }) => c.portalEmail)
|
||||
@@ -42,12 +44,13 @@ export async function getAllTasks(req: AuthRequest, res: Response): Promise<void
|
||||
|
||||
export async function getTaskStats(req: AuthRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
// Für Kundenportal: Filter auf erlaubte Kunden
|
||||
// Für Kundenportal: Filter auf erlaubte Kunden (mit Live-Vollmacht-Check)
|
||||
const allowedIds = await getPortalAllowedCustomerIds(req);
|
||||
let customerPortalCustomerIds: number[] | undefined;
|
||||
let customerPortalEmails: string[] | undefined;
|
||||
|
||||
if (req.user?.isCustomerPortal && req.user.customerId) {
|
||||
customerPortalCustomerIds = [req.user.customerId, ...(req.user.representedCustomerIds || [])];
|
||||
if (allowedIds) {
|
||||
customerPortalCustomerIds = allowedIds;
|
||||
const customers = await customerService.getCustomersByIds(customerPortalCustomerIds);
|
||||
customerPortalEmails = customers
|
||||
.map((c: { id: number; portalEmail: string | null }) => c.portalEmail)
|
||||
@@ -75,33 +78,17 @@ export async function getTasks(req: AuthRequest, res: Response): Promise<void> {
|
||||
const contractId = parseInt(req.params.contractId);
|
||||
const { status } = req.query;
|
||||
|
||||
// Prüfe Zugriff auf den Vertrag
|
||||
const contract = await contractService.getContractById(contractId);
|
||||
if (!contract) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Vertrag nicht gefunden',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
// Zentraler canAccessContract-Check inkl. Live-Vollmacht-Prüfung über
|
||||
// hasAuthorization (Pentest Runde 6 – HOCH-04: widerrufene Vollmachten
|
||||
// hatten vorher weiter Zugriff, weil nur representedCustomerIds-Array
|
||||
// konsultiert wurde, ohne Status-Check).
|
||||
if (!(await canAccessContract(req, res, contractId))) return;
|
||||
|
||||
// Für Kundenportal: Zugriffsprüfung
|
||||
if (req.user?.isCustomerPortal && req.user.customerId) {
|
||||
const allowedCustomerIds = [req.user.customerId, ...(req.user.representedCustomerIds || [])];
|
||||
if (!allowedCustomerIds.includes(contract.customerId)) {
|
||||
res.status(403).json({
|
||||
success: false,
|
||||
error: 'Kein Zugriff auf diesen Vertrag',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Für Kundenportal-Benutzer: Lade E-Mails der erlaubten Kunden
|
||||
// Für Kundenportal-Benutzer: Lade E-Mails der erlaubten Kunden (mit Live-Vollmacht-Check)
|
||||
let customerPortalEmails: string[] | undefined;
|
||||
if (req.user?.isCustomerPortal && req.user.customerId) {
|
||||
const allowedCustomerIds = [req.user.customerId, ...(req.user.representedCustomerIds || [])];
|
||||
const customers = await customerService.getCustomersByIds(allowedCustomerIds);
|
||||
const allowedIds = await getPortalAllowedCustomerIds(req);
|
||||
if (allowedIds) {
|
||||
const customers = await customerService.getCustomersByIds(allowedIds);
|
||||
customerPortalEmails = customers
|
||||
.map((c: { id: number; portalEmail: string | null }) => c.portalEmail)
|
||||
.filter((email: string | null): email is string => !!email);
|
||||
@@ -187,27 +174,8 @@ export async function createSupportTicket(req: AuthRequest, res: Response): Prom
|
||||
return;
|
||||
}
|
||||
|
||||
// Prüfe Zugriff auf den Vertrag
|
||||
const contract = await contractService.getContractById(contractId);
|
||||
if (!contract) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Vertrag nicht gefunden',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
// Zugriffsprüfung für Kundenportal
|
||||
if (req.user?.customerId) {
|
||||
const allowedCustomerIds = [req.user.customerId, ...(req.user.representedCustomerIds || [])];
|
||||
if (!allowedCustomerIds.includes(contract.customerId)) {
|
||||
res.status(403).json({
|
||||
success: false,
|
||||
error: 'Kein Zugriff auf diesen Vertrag',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// canAccessContract inkl. Live-Vollmacht-Prüfung (siehe getTasks).
|
||||
if (!(await canAccessContract(req, res, contractId))) return;
|
||||
|
||||
const createdBy = req.user?.email;
|
||||
|
||||
@@ -376,24 +344,7 @@ export async function createCustomerReply(req: AuthRequest, res: Response): Prom
|
||||
return;
|
||||
}
|
||||
|
||||
// Prüfe ob der Kunde berechtigt ist (eigenes Ticket oder freigegebener Kunde)
|
||||
if (req.user?.isCustomerPortal && req.user.customerId) {
|
||||
const allowedCustomerIds = [req.user.customerId, ...(req.user.representedCustomerIds || [])];
|
||||
const customers = await customerService.getCustomersByIds(allowedCustomerIds);
|
||||
const allowedEmails = customers
|
||||
.map((c: { id: number; portalEmail: string | null }) => c.portalEmail)
|
||||
.filter((email: string | null): email is string => !!email);
|
||||
|
||||
// Task muss entweder visibleInPortal sein ODER vom Kunden erstellt worden sein
|
||||
const isOwnTask = task.createdBy && allowedEmails.includes(task.createdBy);
|
||||
if (!task.visibleInPortal && !isOwnTask) {
|
||||
res.status(403).json({
|
||||
success: false,
|
||||
error: 'Kein Zugriff auf diese Anfrage',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (!req.user?.isCustomerPortal || !req.user.customerId) {
|
||||
res.status(403).json({
|
||||
success: false,
|
||||
error: 'Nur für Kundenportal-Benutzer',
|
||||
@@ -401,6 +352,27 @@ export async function createCustomerReply(req: AuthRequest, res: Response): Prom
|
||||
return;
|
||||
}
|
||||
|
||||
// Strikter Owner-Check über den Vertrag (mit Live-Vollmacht-Prüfung
|
||||
// via hasAuthorization, Pentest Runde 6 – HOCH-04). Damit kann ein
|
||||
// Portal-User keine fremde Task-ID mit visibleInPortal=true abgreifen.
|
||||
if (!(await canAccessContract(req, res, task.contractId))) return;
|
||||
|
||||
// Zusätzlich: portal-User darf nur antworten, wenn die Task von ihm
|
||||
// initiiert wurde ODER explizit für ihn sichtbar markiert ist.
|
||||
const allowedCustomerIds = [req.user.customerId, ...(req.user.representedCustomerIds || [])];
|
||||
const customers = await customerService.getCustomersByIds(allowedCustomerIds);
|
||||
const allowedEmails = customers
|
||||
.map((c: { id: number; portalEmail: string | null }) => c.portalEmail)
|
||||
.filter((email: string | null): email is string => !!email);
|
||||
const isOwnTask = task.createdBy && allowedEmails.includes(task.createdBy);
|
||||
if (!task.visibleInPortal && !isOwnTask) {
|
||||
res.status(403).json({
|
||||
success: false,
|
||||
error: 'Kein Zugriff auf diese Anfrage',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
const createdBy = req.user?.email;
|
||||
|
||||
const subtask = await contractTaskService.createSubtask({
|
||||
|
||||
Reference in New Issue
Block a user