Security-Hardening Runde 8: DNS-Rebinding + Per-File-Ownership
Loose Ends aus Runde 5/7 abgearbeitet.
🛡 DNS-Rebinding-Schutz in SSRF-Guard
- safeResolveHost() löst Hostname zu IPv4+IPv6 auf, prüft jede IP
gegen die Block-Liste, gibt {ip, servername} zurück.
- Caller (test-connection, test-mail-access) übergibt host=ip plus
servername=hostname an die Mail-Services. Damit kann ein zweiter
DNS-Lookup zur Connection-Zeit nicht plötzlich auf interne IPs
umlenken (rebound-Attack).
- ImapCredentials/SmtpCredentials um optionales servername-Feld
erweitert; Services nutzen es als TLS-SNI / Cert-Validation-Hint.
🔒 Per-File-Ownership-Check (DSGVO-Härtung)
- express.static('/api/uploads') ersetzt durch GET /api/files/download
mit Pfad→Resource→Owner-Mapping in fileDownload.service.ts.
- 12 subDir-Mappings (bank-cards, documents, contract-documents,
invoices, cancellation-*, authorizations, business-/commercial-/
privacy-, pdf-templates).
- canAccessCustomer / canAccessContract / Permission-Check je nach
Owner-Typ. Portal-User sieht jetzt nur eigene Dateien, selbst wenn
er fremde Filenames kennt.
- Backwards-Compat: /api/uploads/* bleibt als Shim erhalten, ruft
intern denselben Owner-Check.
- Frontend fileUrl() zeigt auf /api/files/download?path=...&token=...
Live-verifiziert:
- Eigene Datei: 200, random Pfad: 404, ../etc/passwd: 400, kein
Token: 401, Backwards-Compat-Shim: 200.
- DNS-Rebinding: nip.io-Hostname mit interner Target-IP wird via
DNS-Lookup geblockt; gmail.com (legitim) geht durch.
Bewusst nicht gemacht:
- Signierte URLs mit kurzlebigen Download-Tokens – v1.2-Item, da
invasiv für <a href>-Flows ohne JS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+23
-6
@@ -34,6 +34,7 @@ import emailLogRoutes from './routes/emailLog.routes.js';
|
||||
import pdfTemplateRoutes from './routes/pdfTemplate.routes.js';
|
||||
import birthdayRoutes from './routes/birthday.routes.js';
|
||||
import factoryDefaultsRoutes from './routes/factoryDefaults.routes.js';
|
||||
import { downloadFile } from './controllers/fileDownload.controller.js';
|
||||
import { startBirthdayScheduler } from './services/birthdayScheduler.service.js';
|
||||
import { startContractStatusScheduler } from './services/contractStatusScheduler.service.js';
|
||||
import { auditContextMiddleware } from './middleware/auditContext.js';
|
||||
@@ -101,12 +102,28 @@ app.use(express.json({ limit: '5mb' }));
|
||||
app.use(auditContextMiddleware);
|
||||
app.use(auditMiddleware);
|
||||
|
||||
// Statische Dateien für Uploads – NUR für authentifizierte User.
|
||||
// authenticate-Middleware unterstützt ?token=... Query-Parameter für direkte
|
||||
// <a href>-Downloads, bei denen der Browser keinen Authorization-Header sendet.
|
||||
// Ohne diesen Schutz könnte jeder per Datei-Name-Enumeration sensible PDFs
|
||||
// (Ausweise, Kündigungsbestätigungen, Bankkarten) abrufen – DSGVO-GAU.
|
||||
app.use('/api/uploads', authenticate as any, express.static(path.join(process.cwd(), 'uploads')));
|
||||
// Datei-Download mit Per-File-Ownership-Check (ersetzt das alte
|
||||
// `/api/uploads/*` express.static).
|
||||
// Frontend-URLs gehen jetzt über GET /api/files/download?path=/uploads/...
|
||||
// Der Controller mappt den Pfad auf eine Resource (BankCard, Contract, etc.)
|
||||
// und prüft canAccessCustomer/canAccessContract – damit kann ein Portal-Kunde
|
||||
// nur seine eigenen Dateien laden, selbst wenn er fremde Filenames kennt.
|
||||
//
|
||||
// Kompatibilität: das alte /api/uploads/* bleibt erhalten, leitet aber jeden
|
||||
// Request über denselben Owner-Check (kein freier static-Handler mehr).
|
||||
|
||||
// Authentifizierter Datei-Download mit Per-File-Ownership-Check.
|
||||
// Akzeptiert Pfade wie /uploads/bank-cards/<filename> – egal ob als
|
||||
// Query-Parameter oder im Pfad-Suffix. Beide gehen über denselben Handler,
|
||||
// der DB-basiert prüft, ob der eingeloggte User die Resource sehen darf.
|
||||
app.get('/api/files/download', authenticate as any, downloadFile as any);
|
||||
// Backwards-compatibility shim: `/api/uploads/*` sieht weiter aus wie früher
|
||||
// für Bestandsclients/Bookmarks, ruft aber denselben Owner-Check-Handler.
|
||||
app.get('/api/uploads/*', authenticate as any, (req, res, next) => {
|
||||
// Pfad in Query-Param umschreiben, dann an downloadFile weiterreichen
|
||||
req.query.path = req.originalUrl.replace(/^\/api/, '').split('?')[0];
|
||||
return (downloadFile as any)(req, res, next);
|
||||
});
|
||||
|
||||
// Öffentliche Routes (OHNE Authentifizierung)
|
||||
app.use('/api/public/consent', consentPublicRoutes);
|
||||
|
||||
Reference in New Issue
Block a user