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:
@@ -1,20 +1,23 @@
|
||||
/**
|
||||
* Baut eine Download-URL für ein im Backend gespeichertes Upload-File.
|
||||
*
|
||||
* `/api/uploads/*` läuft hinter authenticate-Middleware, aber <a href> und
|
||||
* window.open senden keinen Authorization-Header. Darum hängen wir das JWT
|
||||
* als Query-Parameter an. Die authenticate-Middleware akzeptiert
|
||||
* `?token=<jwt>` neben dem Header.
|
||||
* Geht über `GET /api/files/download?path=...` – der Backend-Controller
|
||||
* macht einen Per-File-Ownership-Check (Pfad → Resource → canAccessCustomer
|
||||
* / canAccessContract). Damit kann auch ein eingeloggter User keine
|
||||
* fremden Dateien abrufen, selbst wenn er den Pfad kennen würde.
|
||||
*
|
||||
* Trade-off: Tokens in URLs landen potenziell in Logs/Referrer. Für eine
|
||||
* saubere Lösung (kurzlebige Download-Tokens) wäre ein separater Endpoint
|
||||
* nötig – TODO für v1.1.
|
||||
* <a href> und window.open senden keinen Authorization-Header, daher
|
||||
* Token als Query-Parameter (auth-Middleware akzeptiert `?token=<jwt>`).
|
||||
*
|
||||
* Trade-off: Tokens in URLs können in Logs/Referrer landen. Eine
|
||||
* sauberere Lösung mit kurzlebigen Download-Tokens (signierte URLs)
|
||||
* wäre v1.1-Item.
|
||||
*/
|
||||
export function fileUrl(path: string | null | undefined): string {
|
||||
if (!path) return '';
|
||||
const token = localStorage.getItem('token');
|
||||
const base = `/api${path.startsWith('/') ? path : '/' + path}`;
|
||||
const normalizedPath = path.startsWith('/') ? path : '/' + path;
|
||||
const base = `/api/files/download?path=${encodeURIComponent(normalizedPath)}`;
|
||||
if (!token) return base;
|
||||
const separator = base.includes('?') ? '&' : '?';
|
||||
return `${base}${separator}token=${encodeURIComponent(token)}`;
|
||||
return `${base}&token=${encodeURIComponent(token)}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user