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:
@@ -124,6 +124,9 @@ function determineSensitivity(resourceType: string): AuditSensitivity {
|
|||||||
User: 'HIGH',
|
User: 'HIGH',
|
||||||
CustomerConsent: 'HIGH',
|
CustomerConsent: 'HIGH',
|
||||||
DataDeletionRequest: 'HIGH',
|
DataDeletionRequest: 'HIGH',
|
||||||
|
// Abgewehrter IDOR-/Cross-Boundary-Zugriffsversuch (canAccess*-403).
|
||||||
|
// HIGH, weil ein Treffer auf gezielte Fremddaten-Enumeration hindeutet.
|
||||||
|
AccessDenied: 'HIGH',
|
||||||
// MEDIUM
|
// MEDIUM
|
||||||
Contract: 'MEDIUM',
|
Contract: 'MEDIUM',
|
||||||
Address: 'MEDIUM',
|
Address: 'MEDIUM',
|
||||||
|
|||||||
@@ -12,10 +12,22 @@ import prisma from '../lib/prisma.js';
|
|||||||
import * as authorizationService from '../services/authorization.service.js';
|
import * as authorizationService from '../services/authorization.service.js';
|
||||||
import { AuthRequest } from '../types/index.js';
|
import { AuthRequest } from '../types/index.js';
|
||||||
import { emit as emitSecurityEvent, contextFromRequest } from '../services/securityMonitor.service.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.
|
* 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 {
|
function emitAccessDenied(req: AuthRequest, label: string, targetId: number | string): void {
|
||||||
const ctx = contextFromRequest(req);
|
const ctx = contextFromRequest(req);
|
||||||
@@ -30,6 +42,16 @@ function emitAccessDenied(req: AuthRequest, label: string, targetId: number | st
|
|||||||
endpoint: ctx.endpoint,
|
endpoint: ctx.endpoint,
|
||||||
details: { resource: label, targetId },
|
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 },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -692,6 +692,19 @@ zunächst gegen den falschen Host getestet wurde
|
|||||||
`kundencenter-stage.stressfrei-wechseln.de`). Auf dem korrekten
|
`kundencenter-stage.stressfrei-wechseln.de`). Auf dem korrekten
|
||||||
Staging-Host reproduzierte sich das Finding sofort.
|
Staging-Host reproduzierte sich das Finding sofort.
|
||||||
|
|
||||||
|
**Nachtrag – Zwei-Stream-Logging der Abwehr:** Beim Retest fiel auf, dass
|
||||||
|
der abgewehrte Zugriff zwar im **SecurityEvent**-Monitoring-Stream landet
|
||||||
|
(`ACCESS_DENIED`, MEDIUM, sichtbar unter
|
||||||
|
`GET /api/monitoring/events?type=ACCESS_DENIED`), aber **nicht** im
|
||||||
|
AuditLog – dort hatte der Pentester zuerst gesucht. Der Monitoring-Stream
|
||||||
|
ist zudem über `DELETE /api/monitoring/events` löschbar und nicht
|
||||||
|
hash-verkettet. `emitAccessDenied` schreibt daher 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, jeweils inkl.
|
||||||
|
Vollmacht-fehlt-Fall).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🔒 Runde 110 – Mass-Assignment-Whitelist auf 7 Katalog-Endpunkten
|
## 🔒 Runde 110 – Mass-Assignment-Whitelist auf 7 Katalog-Endpunkten
|
||||||
|
|||||||
@@ -117,6 +117,14 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
|
|||||||
rendert Vertrags-Provider/Tarif-Namen, React escaped sie als
|
rendert Vertrags-Provider/Tarif-Namen, React escaped sie als
|
||||||
Text. Neue Writes werden zusätzlich per `sanitizeContractBody`
|
Text. Neue Writes werden zusätzlich per `sanitizeContractBody`
|
||||||
(stripHtml) entschärft; der Altwert stammt aus DB-Direkteingabe.
|
(stripHtml) entschärft; der Altwert stammt aus DB-Direkteingabe.
|
||||||
|
- Nachtrag (Retest): Der abgewehrte Zugriff wird nun in ZWEI Streams
|
||||||
|
protokolliert. Bisher nur `SecurityEvent` (`ACCESS_DENIED`, sichtbar
|
||||||
|
unter `/api/monitoring/events`, aber löschbar + nicht hash-verkettet)
|
||||||
|
– der Pentester suchte im AuditLog und fand nichts. `emitAccessDenied`
|
||||||
|
schreibt jetzt zusätzlich einen tamper-evidenten AuditLog-Eintrag
|
||||||
|
(`action: READ`, `resourceType: 'AccessDenied'`, Sensitivity HIGH)
|
||||||
|
für alle `canAccess*`-403. Meine ursprüngliche Formulierung „landet
|
||||||
|
im Audit" war die falsche Tabelle – jetzt stimmt sie.
|
||||||
|
|
||||||
- [x] **👁 Kundenansicht: Toggle „Deaktivierte Verträge anzeigen"**
|
- [x] **👁 Kundenansicht: Toggle „Deaktivierte Verträge anzeigen"**
|
||||||
- Der Vertragsbaum beim Kunden (`CustomerDetail` → Tab Verträge)
|
- Der Vertragsbaum beim Kunden (`CustomerDetail` → Tab Verträge)
|
||||||
|
|||||||
Reference in New Issue
Block a user