Pentest R120: canAccess-403 zusätzlich tamper-evident auditieren
Der Retest deckte auf, dass abgewehrte Cross-Customer-Zugriffe (canAccess*-403) nur im SecurityEvent-Monitoring-Stream landeten (ACCESS_DENIED, /api/monitoring/events) – nicht im AuditLog, wo der Pentester suchte. Der Monitoring-Stream ist zudem löschbar (DELETE /api/monitoring/events) und nicht hash-verkettet. emitAccessDenied schreibt jetzt zusätzlich einen tamper-evidenten AuditLog-Eintrag (action READ, resourceType 'AccessDenied', Sensitivity HIGH), der ein Clearen des Monitoring-Streams überlebt und über die Hash-Kette manipulationssicher ist. Gilt für alle canAccess*-403 (Contract + Customer, inkl. Vollmacht- fehlt-Fall). Kein Enum-/Schema-Change: READ + distinktiver resourceType, filterbar via searchAuditLogs. Korrigiert damit auch meine frühere ungenaue Aussage „landet im Audit" – vorher war das die falsche Tabelle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -12,10 +12,22 @@ import prisma from '../lib/prisma.js';
|
||||
import * as authorizationService from '../services/authorization.service.js';
|
||||
import { AuthRequest } from '../types/index.js';
|
||||
import { emit as emitSecurityEvent, contextFromRequest } from '../services/securityMonitor.service.js';
|
||||
import { logChange } from '../services/audit.service.js';
|
||||
|
||||
/**
|
||||
* Wird intern aufgerufen, wenn ein canAccess*-Check 403 zurückgibt.
|
||||
* Schreibt ein SecurityEvent für Monitoring + spätere Threshold-Detection.
|
||||
*
|
||||
* Schreibt in ZWEI Streams:
|
||||
* 1. SecurityEvent (Monitoring): Alerting + Threshold-Detection, aber
|
||||
* über /api/monitoring/events löschbar und nicht hash-verkettet.
|
||||
* 2. AuditLog (tamper-evident): hash-verketteter Eintrag, überlebt ein
|
||||
* Clearen des Monitoring-Streams. Pentest R120 hat aufgedeckt, dass
|
||||
* der reine SecurityEvent-Stream für Forensik „lautlos genug" wirkt –
|
||||
* ein Cross-Customer-Zugriffsversuch (IDOR) soll manipulationssicher
|
||||
* nachweisbar sein. `action: READ`, weil es ein unautorisierter
|
||||
* Lese-VERSUCH ist (kein eigener Enum-Wert nötig); der distinktive
|
||||
* resourceType `AccessDenied` macht es filterbar
|
||||
* (`searchAuditLogs({ resourceType: 'AccessDenied' })`).
|
||||
*/
|
||||
function emitAccessDenied(req: AuthRequest, label: string, targetId: number | string): void {
|
||||
const ctx = contextFromRequest(req);
|
||||
@@ -30,6 +42,16 @@ function emitAccessDenied(req: AuthRequest, label: string, targetId: number | st
|
||||
endpoint: ctx.endpoint,
|
||||
details: { resource: label, targetId },
|
||||
});
|
||||
// Fire-and-forget; logChange fängt eigene Fehler intern ab und darf die
|
||||
// 403-Response nie blockieren.
|
||||
void logChange({
|
||||
req,
|
||||
action: 'READ',
|
||||
resourceType: 'AccessDenied',
|
||||
resourceId: String(targetId),
|
||||
label: `IDOR-Zugriffsversuch abgewehrt: ${label} #${targetId}`,
|
||||
details: { resource: label, targetId, endpoint: ctx.endpoint },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user