Pentest 2026-05-20 Pen-30-Befunde (MEDIUM+INFO)
30.13 MIME-Extension-XSS (MEDIUM): GET /api/files/download lieferte hochgeladene Dateien via res.sendFile() aus. Da multer nur den client-gemeldeten MIME prueft, konnte eine als application/pdf deklarierte .html-Datei auf Disk landen – Express liest beim Senden den Content-Type aus der Extension (text/html), Browser haette gerendert → Stored XSS. Fix: Content-Disposition: attachment + safe filename. Browser laedt jetzt herunter statt zu rendern, egal welcher Content-Type. UX-Cost ist gering (PDF-Preview offnet halt aus dem Download-Ordner). X-Content-Type-Options: nosniff bleibt zusaetzlich gesetzt. 30.14 SSRF Private-IP-Block opt-in (INFO): ssrfGuard erlaubte private IPs (127/10/172.16/192.168) bewusst, weil On-Prem-Setups Plesk/Dovecot/Postfix lokal laufen lassen. Fuer Cloud-Deployments ist das ein SSRF-Vektor. Neuer Env-Flag SSRF_BLOCK_PRIVATE_IPS=true erweitert die Block-Liste um alle privaten Ranges + ::1 + fc00::/7 + IPv4-mapped + localhost/ ip6-localhost. Default off (on-prem-kompatibel). Live-verifiziert auf dev: - Download-Header: Content-Disposition: attachment + safe filename - Default: 127.0.0.1/10.x/192.168.x/localhost durchgelassen, 169.254.169.254 (Cloud-Metadata) weiter geblockt - SSRF_BLOCK_PRIVATE_IPS=true: alle privaten Ranges geblockt, 8.8.8.8 (legitim) durchgelassen Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -78,7 +78,21 @@ export async function downloadFile(req: AuthRequest, res: Response): Promise<voi
|
||||
return;
|
||||
}
|
||||
|
||||
// Content-Type aus Extension bestimmen (konservativ – Express macht das eh)
|
||||
// Stored-XSS-Schutz (Pentest 2026-05-20 MEDIUM 30.13):
|
||||
// Multer prüfte beim Upload nur den client-gemeldeten MIME-Type.
|
||||
// Eine `.html`-Datei mit `Content-Type: application/pdf` rutschte
|
||||
// durch und wurde mit Original-Extension auf Disk geschrieben.
|
||||
// Beim Download bestimmt res.sendFile() den Content-Type aus der
|
||||
// Extension – also `text/html` – und der Browser hätte das als
|
||||
// Stored-XSS gerendert. `X-Content-Type-Options: nosniff` schützt
|
||||
// nicht, wenn der Server selbst text/html liefert.
|
||||
//
|
||||
// Fix: alle Files via Content-Disposition: attachment ausliefern.
|
||||
// Der Browser lädt herunter statt zu rendern, egal welcher Type.
|
||||
// Für legitime PDF/Bild-Vorschau ist das vertretbar – Browser
|
||||
// öffnen den Download dann eben aus dem Datei-Manager.
|
||||
const filename = path.basename(absolute).replace(/[^A-Za-z0-9._-]/g, '_');
|
||||
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
||||
res.sendFile(absolute);
|
||||
}
|
||||
|
||||
@@ -28,6 +28,32 @@ const BLOCKED_PATTERNS: RegExp[] = [
|
||||
/^ff/i, // IPv6 Multicast
|
||||
];
|
||||
|
||||
// Opt-in für Cloud-Deployments: ALLE privaten IP-Ranges blocken, nicht
|
||||
// nur Cloud-Metadata. On-Prem-Default ist `false`, weil On-Prem-Setups
|
||||
// häufig Plesk/Dovecot/Postfix auf 127.0.0.1 oder im internen Netz
|
||||
// laufen lassen. (Pentest 2026-05-20 INFO 30.14.) Aktivieren mit
|
||||
// `SSRF_BLOCK_PRIVATE_IPS=true` in der Umgebung.
|
||||
const BLOCK_PRIVATE_IPS = (process.env.SSRF_BLOCK_PRIVATE_IPS || '').toLowerCase() === 'true';
|
||||
|
||||
const PRIVATE_IP_PATTERNS: RegExp[] = [
|
||||
/^127\./, // 127.0.0.0/8 Loopback
|
||||
/^10\./, // 10.0.0.0/8
|
||||
/^192\.168\./, // 192.168.0.0/16
|
||||
/^172\.(1[6-9]|2\d|3[01])\./, // 172.16.0.0/12
|
||||
/^::1$/, // IPv6 Loopback
|
||||
/^::ffff:127\./i, // IPv4-mapped Loopback
|
||||
/^::ffff:10\./i, // IPv4-mapped 10/8
|
||||
/^::ffff:192\.168\./i, // IPv4-mapped 192.168/16
|
||||
/^::ffff:172\.(1[6-9]|2\d|3[01])\./i,
|
||||
/^f[cd]/i, // fc00::/7 Unique-Local
|
||||
];
|
||||
|
||||
const PRIVATE_HOSTNAMES = new Set([
|
||||
'localhost',
|
||||
'ip6-localhost',
|
||||
'ip6-loopback',
|
||||
]);
|
||||
|
||||
const BLOCKED_HOSTNAMES = new Set([
|
||||
'metadata.google.internal',
|
||||
'metadata.goog',
|
||||
@@ -43,6 +69,12 @@ export function isBlockedSsrfHost(host: string | null | undefined): boolean {
|
||||
for (const pattern of BLOCKED_PATTERNS) {
|
||||
if (pattern.test(h)) return true;
|
||||
}
|
||||
if (BLOCK_PRIVATE_IPS) {
|
||||
if (PRIVATE_HOSTNAMES.has(h)) return true;
|
||||
for (const pattern of PRIVATE_IP_PATTERNS) {
|
||||
if (pattern.test(h)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -120,6 +120,35 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
|
||||
- **Live-verifiziert**: 4867 Datensätze + 1 Datei in 13.2s
|
||||
wiederhergestellt, Log-Modal zeigt den vollständigen Verlauf.
|
||||
|
||||
- [x] **🛡️ Pentest 2026-05-20 Pen-30-Befunde (MEDIUM+INFO)**
|
||||
- **30.13 MIME-Extension-XSS** (MEDIUM): `GET /api/files/download`
|
||||
lieferte hochgeladene Dateien via `res.sendFile` aus. Da multer
|
||||
nur den client-gemeldeten MIME prüft, konnte eine als
|
||||
`application/pdf` deklarierte `.html`-Datei auf Disk landen –
|
||||
Express bestimmt beim Senden den Content-Type aus der Extension
|
||||
(`.html` → `text/html`) und der Browser hätte gerendert. Stored
|
||||
XSS für eingeloggte Empfänger. Fix: `Content-Disposition:
|
||||
attachment; filename=<safe>` + bestehendes `X-Content-Type-
|
||||
Options: nosniff`. Browser lädt jetzt herunter statt zu rendern,
|
||||
selbst wenn der Type stimmt. Filename wird auf
|
||||
`[A-Za-z0-9._-]` gesäubert.
|
||||
- **30.14 SSRF Private-IP-Block opt-in** (INFO): Neuer Env-Flag
|
||||
`SSRF_BLOCK_PRIVATE_IPS=true` erweitert die SSRF-Block-Liste auf
|
||||
127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, ::1,
|
||||
fc00::/7 + IPv4-mapped Varianten + Hostnamen "localhost"/
|
||||
"ip6-localhost". Default off, damit On-Prem-Installationen
|
||||
(Plesk/Dovecot auf 127.0.0.1) nicht brechen. Cloud-Deployments
|
||||
setzen den Flag.
|
||||
- **Live-verifiziert** auf dev:
|
||||
- Upload + Download: Header zeigt
|
||||
`Content-Disposition: attachment; filename="…"` +
|
||||
`X-Content-Type-Options: nosniff`
|
||||
- Default-ssrfGuard: 127.0.0.1 / 10.x / 192.168.x / localhost → false
|
||||
(durchgelassen für on-prem); 169.254.169.254 → true (Cloud-
|
||||
Metadata weiter geblockt)
|
||||
- Mit `SSRF_BLOCK_PRIVATE_IPS=true`: alle privaten Ranges → true;
|
||||
8.8.8.8 (legit public) → false
|
||||
|
||||
- [x] **🛡️ Pentest 2026-05-20 Pen-29-Befunde (LOW/INFO)**
|
||||
- **28.1 Restarbeit**: `DANGEROUS_URI_SCHEMES` jetzt vollständig –
|
||||
`blob:`, `about:`, `ws:`, `wss:`, `ldap:`, `dict:` ergänzt. Bewusst
|
||||
|
||||
Reference in New Issue
Block a user