Pentest 2026-05-20 Pen-28-Befunde (LOW/INFO)

28.1 URI-Schema unvollstaendig:
DANGEROUS_URI_SCHEMES erweitert um file:/ftp: – "ftp://evil.com/x.js"
und "file:///etc/passwd" wurden vorher in companyName akzeptiert.

28.2 HTML-Entity-Decoding-Bypass:
stripHtml() lief direkt ueber den Roh-String, "javascript:",
"<script>" und "<script>" umgingen die Regex.
decodeHtmlEntities() dekodiert jetzt numerische (decimal+hex) +
gaengige named entities VOR dem Tag-/URI-Strip.

28.3 Vollmacht-Upload Magic-Byte-Check:
multer pruefte nur client-MIME, HTML/PHP/Shell-Scripts kamen als
application/pdf durch. uploadAuthorizationDocument liest jetzt die
ersten 5 Bytes und verlangt "%PDF-", sonst Loeschen + 400.

28.4 Rate-Limit auf /api/public/consent:
30 Requests pro IP pro 15min. Brute-Force-sicher war der 128-bit-
UUID-Hash schon, aber ohne Limit konnte ein Angreifer das System
mit Audit-Log- und Mail-Spam belasten.

Live-verifiziert auf dev: alle vier Bypaesse blockiert, legitime
Eingaben unangetastet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-20 17:46:15 +02:00
parent 8e48d3b432
commit 65ec07e274
5 changed files with 121 additions and 7 deletions
@@ -891,6 +891,33 @@ 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,
// ein Angreifer kann beliebige Daten als "application/pdf" hochladen.
// PDF beginnt mit "%PDF-" (0x25 0x50 0x44 0x46 0x2D). Wenn nicht,
// gleich wegwerfen. (Pentest 2026-05-20 LOW 28.3)
const PDF_MAGIC = Buffer.from([0x25, 0x50, 0x44, 0x46, 0x2d]); // %PDF-
try {
const fd = fs.openSync(req.file.path, 'r');
const head = Buffer.alloc(5);
fs.readSync(fd, head, 0, 5, 0);
fs.closeSync(fd);
if (!head.equals(PDF_MAGIC)) {
// Hochgeladene Nicht-PDF-Datei sofort wieder löschen, sonst
// bleibt der Müll im uploads/-Volume.
try { fs.unlinkSync(req.file.path); } catch { /* ignore */ }
return res.status(400).json({
success: false,
error: 'Datei ist keine gültige PDF (Magic-Bytes fehlen).',
});
}
} catch (e) {
try { fs.unlinkSync(req.file.path); } catch { /* ignore */ }
return res.status(400).json({
success: false,
error: 'Hochgeladene Datei konnte nicht gelesen werden.',
});
}
const documentPath = `/uploads/authorizations/${req.file.filename}`; const documentPath = `/uploads/authorizations/${req.file.filename}`;
const auth = await authorizationService.updateAuthorizationDocument( const auth = await authorizationService.updateAuthorizationDocument(
customerId, customerId,
+23
View File
@@ -81,3 +81,26 @@ export const passwordResetRateLimiter = rateLimit({
res.status(options.statusCode).json(options.message); res.status(options.statusCode).json(options.message);
}, },
}); });
/**
* Public-Consent-Endpoints (/api/public/consent/:hash[/grant|/pdf]) sind
* unauthenticated. Der hash ist 128-bit-UUID → kein Brute-Force-Risk,
* aber DoS-Vektor: ohne Limit könnte ein Angreifer endlos POSTen und
* den Service durch Audit-Log-Spam + Mail-Versand belasten.
* (Pentest 2026-05-20 INFO 28.4). 30 Requests pro 15 min pro IP reicht
* für legitime Kunden weit aus.
*/
export const publicConsentRateLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
limit: 30,
standardHeaders: 'draft-7',
legacyHeaders: false,
message: {
success: false,
error: 'Zu viele Anfragen. Bitte in 15 Minuten erneut versuchen.',
},
handler: (req, res, _next, options) => {
onLimitReached('public-consent', 'MEDIUM')(req, res);
res.status(options.statusCode).json(options.message);
},
});
+5 -1
View File
@@ -1,9 +1,13 @@
import { Router } from 'express'; import { Router } from 'express';
import * as controller from '../controllers/consent-public.controller.js'; import * as controller from '../controllers/consent-public.controller.js';
import { publicConsentRateLimiter } from '../middleware/rateLimit.js';
const router = Router(); const router = Router();
// Öffentliche Routes - KEINE Authentifizierung erforderlich // Öffentliche Routes - KEINE Authentifizierung erforderlich.
// Rate-Limit gegen DoS siehe publicConsentRateLimiter
// (Pentest 2026-05-20 INFO 28.4).
router.use(publicConsentRateLimiter);
router.get('/:hash', controller.getConsentPage); router.get('/:hash', controller.getConsentPage);
router.post('/:hash/grant', controller.grantAllConsents); router.post('/:hash/grant', controller.grantAllConsents);
router.get('/:hash/pdf', controller.getConsentPdf); router.get('/:hash/pdf', controller.getConsentPdf);
+32 -6
View File
@@ -241,16 +241,42 @@ const USER_CREATE_FIELDS = [
* companyName landete vorher ungefiltert in der DB. * companyName landete vorher ungefiltert in der DB.
* *
* Pentest 2026-05-20 (LOW): zusätzlich werden Skript-URI-Schemata * Pentest 2026-05-20 (LOW): zusätzlich werden Skript-URI-Schemata
* unschädlich gemacht (`javascript:`, `data:`, `vbscript:`). Plain-Text- * unschädlich gemacht (`javascript:`, `data:`, `vbscript:`, `file:`,
* Felder enthalten legitime URLs ohnehin selten; ein gespeicherter * `ftp:`). Plain-Text-Felder enthalten legitime URLs ohnehin selten;
* `javascript:alert(1)` würde ansonsten in einem `<a href={value}>` * ein gespeicherter `javascript:alert(1)` würde ansonsten in einem
* sofort feuern. * `<a href={value}>` sofort feuern. `file:` + `ftp:` ergänzt nach
* Pentest 28.1 kein direkter XSS, aber Data-Exfil/Lokalpfad-Vektor.
*
* Pentest 28.2: HTML-Entity-Decoding VOR dem Strip, sonst umgehen
* `&#106;avascript:` und `&#60;script&#62;` die Regex.
*/ */
const DANGEROUS_URI_SCHEMES = /(?:javascript|data|vbscript)\s*:/gi; const DANGEROUS_URI_SCHEMES = /(?:javascript|data|vbscript|file|ftp)\s*:/gi;
function decodeHtmlEntities(s: string): string {
return s
// Numeric decimal: &#106; → 'j'
.replace(/&#(\d+);?/g, (_m, code) => {
const n = parseInt(code, 10);
return n >= 0 && n <= 0x10FFFF ? String.fromCodePoint(n) : '';
})
// Numeric hex: &#x6A; → 'j'
.replace(/&#x([0-9a-fA-F]+);?/g, (_m, code) => {
const n = parseInt(code, 16);
return n >= 0 && n <= 0x10FFFF ? String.fromCodePoint(n) : '';
})
// Häufige Named-Entities (bewusst klein gehalten wir wollen nur
// XSS-relevante Bypässe verhindern, nicht jeden Entity-Sonderfall).
// &amp; ZULETZT, sonst doppel-dekodiert (`&amp;lt;` → `<`).
.replace(/&lt;/gi, '<')
.replace(/&gt;/gi, '>')
.replace(/&quot;/gi, '"')
.replace(/&#?apos;/gi, "'")
.replace(/&amp;/gi, '&');
}
export function stripHtml(value: unknown): unknown { export function stripHtml(value: unknown): unknown {
if (typeof value !== 'string') return value; if (typeof value !== 'string') return value;
return value return decodeHtmlEntities(value)
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '') .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '') .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
.replace(/<\/?[a-z][^>]*>/gi, '') .replace(/<\/?[a-z][^>]*>/gi, '')
+34
View File
@@ -120,6 +120,40 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
- **Live-verifiziert**: 4867 Datensätze + 1 Datei in 13.2s - **Live-verifiziert**: 4867 Datensätze + 1 Datei in 13.2s
wiederhergestellt, Log-Modal zeigt den vollständigen Verlauf. wiederhergestellt, Log-Modal zeigt den vollständigen Verlauf.
- [x] **🛡️ Pentest 2026-05-20 Pen-28-Befunde (LOW/INFO)**
- **28.1 URI-Schema unvollständig**: `DANGEROUS_URI_SCHEMES`-Regex
erweitert um `file:` und `ftp:`. `ftp://evil.com/x.js` und
`file:///etc/passwd` in companyName werden jetzt zu
`blocked://...` bzw. `blocked:///...` neutralisiert.
- **28.2 HTML-Entity-Decoding-Bypass**: `stripHtml` lief vorher
direkt über den Roh-String, sodass `&#106;avascript:`,
`&#x3C;script&#x3E;` und `&lt;script&gt;` an der Regex
vorbeischlüpften. Neuer `decodeHtmlEntities()` läuft VOR dem
Strip und dekodiert numerische (`&#NN;`/`&#xHH;`) + gängige
Named-Entities (lt/gt/quot/apos/amp). Danach greift die normale
Tag- und URI-Säuberung wieder.
- **28.3 Vollmacht-Upload Magic-Byte-Check**: multer prüfte nur
den client-gemeldeten MIME-Type, eine HTML/PHP-Datei als
`application/pdf` kam durch. Neuer Check liest die ersten 5
Bytes nach dem Upload und verlangt `%PDF-` sonst wird die
Datei gelöscht und 400 geliefert. Greift bevor irgendwas in
der DB landet.
- **28.4 Rate-Limit auf /api/public/consent**: 30 Requests pro
IP pro 15 Minuten. Brute-Force-sicher war der 128-Bit-UUID-Hash
schon, aber ohne Limit konnte ein Angreifer das System per
POST-Spam mit Audit-Logs und Mail-Versand belasten. Neuer
`publicConsentRateLimiter` greift jetzt auf alle drei
Sub-Routes (`/:hash`, `/:hash/grant`, `/:hash/pdf`).
- **Live-verifiziert** auf dev:
- `ftp://evil.com/x.js``blocked://evil.com/x.js`
- `file:///etc/passwd``blocked:///etc/passwd`
- `&#106;avascript:alert(1)``blocked:alert(1)`
- `&#x3C;script&#x3E;alert(1)&#x3C;/script&#x3E;OK``OK`
- `&lt;script&gt;bad()&lt;/script&gt;Legit``Legit`
- HTML-Datei als PDF hochgeladen → 400 + Datei gelöscht
- Echtes PDF (Magic-Bytes ok) → 200
- 35× POST auf public-consent → Req 130 = 404, Req 31+ = 429
- [x] **🧹 Pentest 2026-05-20 LOW/INFO-Sammelfix** - [x] **🧹 Pentest 2026-05-20 LOW/INFO-Sammelfix**
- **27.1 Path-Traversal-Strings in DB**: `cleanupConsents` validierte - **27.1 Path-Traversal-Strings in DB**: `cleanupConsents` validierte
`documentPath` zuvor nur per stripHtml, was `../../../etc/passwd` `documentPath` zuvor nur per stripHtml, was `../../../etc/passwd`