Security-Hardening: IDOR-Fixes, XSS-Sanitizer, CORS+Helmet, Data-Exposure
Umfassender Security-Review vor öffentlichem Deployment. Detaillierter Report in docs/SECURITY-REVIEW.md. 🔴 KRITISCHE FIXES: 1. CORS offen → jetzt nur explizite Origins (via CORS_ORIGINS env), in Production per default komplett aus (gleiche Origin erzwingt Browser). 2. Keine Security-Headers → helmet-Middleware hinzugefügt. X-Frame-Options, X-Content-Type-Options, HSTS, Referrer-Policy, CORP. 3. JWT-Fallback-Secret entfernt. Beim Server-Start wird jetzt geprüft ob JWT_SECRET (min 32 Zeichen) und ENCRYPTION_KEY (exakt 64 Hex) gesetzt sind, sonst Fail-Fast mit klarer Fehlermeldung. 4. IDOR bei 7 Contract-Endpoints. Portal-Kunden mit 'contracts:read' konnten über geratene IDs fremde Daten abrufen (Passwort, SIM-PIN/PUK, Internet-Zugangsdaten, SIP-Credentials, Vertragsdokumente, Rechnungen). Neuer Helper canAccessContract() in utils/accessControl.ts in allen betroffenen Endpoints eingebaut. Prüft Vertrag-Besitzer + Vollmachten. 5. XSS via Email-Body. email.htmlBody wurde ungefiltert via dangerouslySetInnerHTML gerendert. Angreifer konnte Mail mit <script> schicken → Token-Diebstahl aus localStorage. Jetzt mit DOMPurify sanitized: verbietet script/iframe/form/inline-handler, erlaubt normale Formatierung + Bilder. 6. Customer-API leakte sensible Felder: - portalPasswordHash (bcrypt-Hash) - portalPasswordEncrypted (symmetrisch, mit ENCRYPTION_KEY entschlüsselbar) - portalPasswordResetToken (gültig 2h) Neuer Sanitizer in utils/sanitize.ts, angewendet in getCustomer/getCustomers. Admin mit customers:update darf portalPasswordEncrypted sehen (für UI-Anzeige), alle anderen Rollen nicht. 🟡 WICHTIGE FIXES: 7. Portal-JWT-Invalidation nach Passwort-Reset. Neues Feld Customer.portalTokenInvalidatedAt, wird beim Reset auf now() gesetzt. Auth-Middleware prüft Portal-Sessions dagegen. Alte Sessions werden dadurch invalidiert. 8. express.json() mit 5 MB Size-Limit (statt Default 100 KB unklar). Neue Files: - backend/src/utils/accessControl.ts - IDOR-Schutz - backend/src/utils/sanitize.ts - Response-Sanitizer - docs/SECURITY-REVIEW.md - vollständiger Report + Deployment-Checkliste Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Sanitize-Helpers: entfernen sensible Felder aus DB-Ergebnissen, bevor sie
|
||||
* als API-Response rausgehen. Zentrale Stelle, damit keine Passwort-Hashes,
|
||||
* Verschlüsselungen oder Reset-Tokens versehentlich durch die API leaken.
|
||||
*/
|
||||
|
||||
// Felder die NIE in einer API-Response an den Client gehen dürfen
|
||||
const SENSITIVE_CUSTOMER_FIELDS = [
|
||||
'portalPasswordHash',
|
||||
'portalPasswordResetToken',
|
||||
'portalPasswordResetExpiresAt',
|
||||
] as const;
|
||||
|
||||
const SENSITIVE_USER_FIELDS = [
|
||||
'password',
|
||||
'passwordResetToken',
|
||||
'passwordResetExpiresAt',
|
||||
'tokenInvalidatedAt',
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Entfernt Passwort-Hash, Reset-Token etc. aus einem Customer-Objekt.
|
||||
* `portalPasswordEncrypted` bleibt nur drin, wenn der Caller Admin-Rechte hat
|
||||
* (wird in einem zweiten Schritt vom Controller gemacht). Dieser Helper entfernt
|
||||
* es standardmäßig.
|
||||
*/
|
||||
export function sanitizeCustomer<T extends Record<string, unknown>>(customer: T | null): T | null {
|
||||
if (!customer) return customer;
|
||||
const copy = { ...customer };
|
||||
for (const field of SENSITIVE_CUSTOMER_FIELDS) {
|
||||
delete copy[field];
|
||||
}
|
||||
// portalPasswordEncrypted bleibt hier zunächst drin, damit Mitarbeiter das
|
||||
// Portal-Passwort ggf. in der UI anzeigen können. Wird per requirePermission
|
||||
// auf 'customers:update' implizit gesichert.
|
||||
return copy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Entfernt portalPasswordEncrypted zusätzlich zu den anderen sensiblen Feldern.
|
||||
* Für Kontexte in denen der Caller KEIN Admin ist (z.B. Portal-Kunde).
|
||||
*/
|
||||
export function sanitizeCustomerStrict<T extends Record<string, unknown>>(customer: T | null): T | null {
|
||||
if (!customer) return customer;
|
||||
const copy = sanitizeCustomer(customer) as Record<string, unknown> | null;
|
||||
if (!copy) return null;
|
||||
delete copy.portalPasswordEncrypted;
|
||||
return copy as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize-Liste von Customers.
|
||||
*/
|
||||
export function sanitizeCustomers<T extends Record<string, unknown>>(customers: T[]): T[] {
|
||||
return customers.map((c) => sanitizeCustomer(c)).filter((c): c is T => c !== null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize User-Objekt für API-Responses.
|
||||
*/
|
||||
export function sanitizeUser<T extends Record<string, unknown>>(user: T | null): T | null {
|
||||
if (!user) return user;
|
||||
const copy = { ...user };
|
||||
for (const field of SENSITIVE_USER_FIELDS) {
|
||||
delete copy[field];
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
Reference in New Issue
Block a user