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:
@@ -15,6 +15,10 @@ export interface SmtpCredentials {
|
||||
password: string;
|
||||
encryption?: MailEncryption; // SSL, STARTTLS oder NONE (Standard: SSL)
|
||||
allowSelfSignedCerts?: boolean; // Selbstsignierte Zertifikate erlauben
|
||||
// DNS-Rebinding-Schutz: Caller hat den Hostname zu IP aufgelöst und
|
||||
// setzt host=IP, servername=originalHostname für TLS-SNI / Cert-Validation.
|
||||
// Damit kann ein zweiter DNS-Lookup nicht plötzlich auf eine interne IP zeigen.
|
||||
servername?: string;
|
||||
}
|
||||
|
||||
// Anhang-Interface
|
||||
@@ -94,7 +98,7 @@ export async function sendEmail(
|
||||
port: number;
|
||||
secure: boolean;
|
||||
auth: { user: string; pass: string };
|
||||
tls?: { rejectUnauthorized: boolean; minVersion?: string; ciphers?: string };
|
||||
tls?: { rejectUnauthorized: boolean; minVersion?: string; ciphers?: string; servername?: string };
|
||||
ignoreTLS?: boolean;
|
||||
requireTLS?: boolean;
|
||||
connectionTimeout: number;
|
||||
@@ -116,6 +120,11 @@ export async function sendEmail(
|
||||
// TLS-Optionen nur wenn nicht NONE
|
||||
if (encryption !== 'NONE') {
|
||||
transportOptions.tls = { rejectUnauthorized };
|
||||
// DNS-Rebinding-Schutz: wenn host eine IP ist, der ursprüngliche
|
||||
// Hostname für SNI/Cert-Validation explizit setzen.
|
||||
if (credentials.servername) {
|
||||
transportOptions.tls.servername = credentials.servername;
|
||||
}
|
||||
if (credentials.allowSelfSignedCerts) {
|
||||
// Auch ältere TLS-Versionen + legacy Cipher-Suites für alte Server zulassen
|
||||
transportOptions.tls.minVersion = 'TLSv1';
|
||||
@@ -303,7 +312,7 @@ export async function testSmtpConnection(credentials: SmtpCredentials): Promise<
|
||||
port: number;
|
||||
secure: boolean;
|
||||
auth: { user: string; pass: string };
|
||||
tls?: { rejectUnauthorized: boolean; minVersion?: string; ciphers?: string };
|
||||
tls?: { rejectUnauthorized: boolean; minVersion?: string; ciphers?: string; servername?: string };
|
||||
ignoreTLS?: boolean;
|
||||
connectionTimeout: number;
|
||||
greetingTimeout: number;
|
||||
@@ -321,6 +330,9 @@ export async function testSmtpConnection(credentials: SmtpCredentials): Promise<
|
||||
|
||||
if (encryption !== 'NONE') {
|
||||
transportOptions.tls = { rejectUnauthorized };
|
||||
if (credentials.servername) {
|
||||
transportOptions.tls.servername = credentials.servername;
|
||||
}
|
||||
} else {
|
||||
transportOptions.ignoreTLS = true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user