Pentest 2026-05-20 Pen-29-Befunde (LOW/INFO)
28.1 Restarbeit (URI-Schemata):
DANGEROUS_URI_SCHEMES jetzt vollstaendig – blob:, about:, ws:, wss:,
ldap:, dict: ergaenzt. http(s):, mailto:, tel: bewusst nicht
geblockt (legitime URLs in Notizfeldern).
29.1 Cyrillic-Homoglyph:
"jаvascript:" mit U+0430 lief durch die Regex. HOMOGLYPH_TO_ASCII-
Map (а→a, е→e, о→o, …, 13 Eintraege) wird VOR dem Scheme-Strip
angewendet.
29.2 Percent-Encoding:
"java%73cript:" und "java%2573cript:" umgingen den Filter.
percentDecode() laeuft jetzt iterativ bis zu 5 Runden.
29.3 Zero-Width-Joiner:
"javascript:" mit U+200B/200C/200D etc. zerteilte die Regex-
Matches. ZERO_WIDTH_CHARS-Regex strippt alle unsichtbaren Unicode-
Steuerzeichen, bevor irgendwas anderes laeuft.
28.3 Partial (PDF-Validierung tiefer):
Magic-Bytes allein reichten nicht – "%PDF-1.4\n#!/bin/bash" kam
durch. Jetzt zusaetzlich %%EOF-Marker in den letzten 1 KB +
Pattern-Scan der ersten 4 KB auf #!/, <script, <?php, <%, "MZ "
(PE-Header).
29.4 Email-Format-Validator:
neuer isValidEmail() lehnt Whitespace/Newlines (SMTP-Header-
Injection-Vektor) und Format-Muell ab. Verdrahtet in
create/update Customer + User + updatePortalSettings.
29.5 GET /api/providers/email 500 -> 404:
parseInt("email") = NaN, Prisma crashte. Controller validiert jetzt
Number.isFinite(id) und liefert 404.
Live-verifiziert auf dev: 13 Test-Cases (alle Schema-Varianten,
Homoglyphe, Percent, ZWJ, PDF-Validierung, Email-Format,
/providers/email) – alle erwarteten Antworten.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
|||||||
sanitizeCustomerStrict,
|
sanitizeCustomerStrict,
|
||||||
pickCustomerCreate,
|
pickCustomerCreate,
|
||||||
pickCustomerUpdate,
|
pickCustomerUpdate,
|
||||||
|
isValidEmail,
|
||||||
} from '../utils/sanitize.js';
|
} from '../utils/sanitize.js';
|
||||||
import {
|
import {
|
||||||
canAccessMeter,
|
canAccessMeter,
|
||||||
@@ -79,6 +80,16 @@ export async function createCustomer(req: Request, res: Response): Promise<void>
|
|||||||
try {
|
try {
|
||||||
// Whitelist: nur erlaubte Felder aus req.body übernehmen
|
// Whitelist: nur erlaubte Felder aus req.body übernehmen
|
||||||
const data: any = pickCustomerCreate(req.body);
|
const data: any = pickCustomerCreate(req.body);
|
||||||
|
// Email-Format prüfen, sonst landet "test@x.de\nBcc:evil@..." als
|
||||||
|
// SMTP-Header-Injection-Vektor in der DB (Pentest 29.4).
|
||||||
|
if (data.email && !isValidEmail(data.email)) {
|
||||||
|
res.status(400).json({ success: false, error: 'Ungültiges E-Mail-Format' } as ApiResponse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.portalEmail && !isValidEmail(data.portalEmail)) {
|
||||||
|
res.status(400).json({ success: false, error: 'Ungültiges Portal-E-Mail-Format' } as ApiResponse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Convert birthDate string to Date if present
|
// Convert birthDate string to Date if present
|
||||||
if (data.birthDate) {
|
if (data.birthDate) {
|
||||||
data.birthDate = new Date(data.birthDate);
|
data.birthDate = new Date(data.birthDate);
|
||||||
@@ -110,6 +121,15 @@ export async function updateCustomer(req: Request, res: Response): Promise<void>
|
|||||||
try {
|
try {
|
||||||
const customerId = parseInt(req.params.id);
|
const customerId = parseInt(req.params.id);
|
||||||
// Whitelist: nur erlaubte Felder aus req.body übernehmen (Mass-Assignment-Schutz)
|
// Whitelist: nur erlaubte Felder aus req.body übernehmen (Mass-Assignment-Schutz)
|
||||||
|
// Email-Validierung gegen SMTP-Header-Injection (Pentest 29.4)
|
||||||
|
if (req.body?.email && !isValidEmail(req.body.email)) {
|
||||||
|
res.status(400).json({ success: false, error: 'Ungültiges E-Mail-Format' } as ApiResponse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (req.body?.portalEmail && !isValidEmail(req.body.portalEmail)) {
|
||||||
|
res.status(400).json({ success: false, error: 'Ungültiges Portal-E-Mail-Format' } as ApiResponse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const data: any = pickCustomerUpdate(req.body);
|
const data: any = pickCustomerUpdate(req.body);
|
||||||
|
|
||||||
// Vorherigen Stand laden für Audit
|
// Vorherigen Stand laden für Audit
|
||||||
@@ -937,6 +957,11 @@ export async function updatePortalSettings(req: Request, res: Response): Promise
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { portalEnabled, portalEmail } = body;
|
const { portalEnabled, portalEmail } = body;
|
||||||
|
// Email-Validierung gegen SMTP-Header-Injection (Pentest 29.4)
|
||||||
|
if (portalEmail && !isValidEmail(portalEmail)) {
|
||||||
|
res.status(400).json({ success: false, error: 'Ungültiges Portal-E-Mail-Format' } as ApiResponse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Vorherigen Stand laden für Audit
|
// Vorherigen Stand laden für Audit
|
||||||
const before = await prisma.customer.findUnique({
|
const before = await prisma.customer.findUnique({
|
||||||
|
|||||||
@@ -891,26 +891,71 @@ export async function uploadAuthorizationDocument(req: AuthRequest, res: Respons
|
|||||||
return res.status(400).json({ success: false, error: 'Keine Datei hochgeladen' });
|
return res.status(400).json({ success: false, error: 'Keine Datei hochgeladen' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Magic-Byte-Check: multer prüft nur den client-gemeldeten MIME-Type,
|
// Strukturelle PDF-Validierung: multer prüft nur den client-gemeldeten
|
||||||
// ein Angreifer kann beliebige Daten als "application/pdf" hochladen.
|
// MIME-Type, ein Angreifer kann beliebige Daten als "application/pdf"
|
||||||
// PDF beginnt mit "%PDF-" (0x25 0x50 0x44 0x46 0x2D). Wenn nicht,
|
// hochladen. Wir verlangen:
|
||||||
// gleich wegwerfen. (Pentest 2026-05-20 LOW 28.3)
|
// 1) Magic-Bytes "%PDF-" am Anfang
|
||||||
|
// 2) "%%EOF"-Marker in den letzten 1024 Bytes (Standard-PDF-Ende)
|
||||||
|
// 3) keinen Shebang ("#!") und kein "<script"/"<?php" in den
|
||||||
|
// ersten 4 KB (Pentest 28.3 Partial: "%PDF-1.4\n#!/bin/bash"
|
||||||
|
// passierte die reine Magic-Byte-Prüfung).
|
||||||
|
// Wer trotzdem eine PDF mit eingebettetem JS hochlädt, bekommt das
|
||||||
|
// hier nicht erkannt – aber das ist Adobe-Acrobat-Risiko und nicht
|
||||||
|
// mehr ein CRM-Backend-Bug. Hier geht's um simple File-Type-Spoofs.
|
||||||
const PDF_MAGIC = Buffer.from([0x25, 0x50, 0x44, 0x46, 0x2d]); // %PDF-
|
const PDF_MAGIC = Buffer.from([0x25, 0x50, 0x44, 0x46, 0x2d]); // %PDF-
|
||||||
try {
|
try {
|
||||||
|
const stat = fs.statSync(req.file.path);
|
||||||
const fd = fs.openSync(req.file.path, 'r');
|
const fd = fs.openSync(req.file.path, 'r');
|
||||||
|
|
||||||
|
// Header
|
||||||
const head = Buffer.alloc(5);
|
const head = Buffer.alloc(5);
|
||||||
fs.readSync(fd, head, 0, 5, 0);
|
fs.readSync(fd, head, 0, 5, 0);
|
||||||
fs.closeSync(fd);
|
|
||||||
if (!head.equals(PDF_MAGIC)) {
|
if (!head.equals(PDF_MAGIC)) {
|
||||||
// Hochgeladene Nicht-PDF-Datei sofort wieder löschen, sonst
|
fs.closeSync(fd);
|
||||||
// bleibt der Müll im uploads/-Volume.
|
|
||||||
try { fs.unlinkSync(req.file.path); } catch { /* ignore */ }
|
try { fs.unlinkSync(req.file.path); } catch { /* ignore */ }
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Datei ist keine gültige PDF (Magic-Bytes fehlen).',
|
error: 'Datei ist keine gültige PDF (Magic-Bytes fehlen).',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
|
// Erste 4 KB scannen auf verbotene Marker (Shell-Script,
|
||||||
|
// HTML/PHP-Payload). Ein echtes PDF enthält am Anfang nur
|
||||||
|
// Binärdaten + ein paar ASCII-Marker, "#!" / "<script" sind
|
||||||
|
// klare Spoof-Indikatoren.
|
||||||
|
const headSize = Math.min(stat.size, 4096);
|
||||||
|
const headBuf = Buffer.alloc(headSize);
|
||||||
|
fs.readSync(fd, headBuf, 0, headSize, 0);
|
||||||
|
const headStr = headBuf.toString('latin1').toLowerCase();
|
||||||
|
const forbidden = ['#!/', '<script', '<?php', '<%', 'mz | ||||||