Pentest R87: Identifier-Whitelist vor stripHtml ziehen

R87.1 LOW: stripHtml lief im R86-Fix VOR der Whitelist.
`<b>bold</b>` ging als `"bold"` mit 200 OK durch,
`<script>…</script>` reduzierte auf leeren String → null in DB
→ vorheriger Wert ohne Fehlermeldung überschrieben.

Fix: validateContractIdentifier läuft jetzt direkt gegen den
Raw-Input für die fünf Identifier-Felder. Die strikte Whitelist
lehnt eh alles ab, was stripHtml normalerweise auffangen würde
(Tags, Schemes, Zero-Width, Homoglyphe, Percent-Encoding) –
Defense-in-Depth bleibt, nur ehrlich (400 statt silent-200).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-21 12:50:45 +02:00
parent c8b86ca9a7
commit 26959ec909
3 changed files with 64 additions and 3 deletions
+12 -3
View File
@@ -36,14 +36,23 @@ function sanitizeContractBody(body: unknown, parentKey?: string): unknown {
if (body === null || body === undefined) return body;
if (typeof body === 'string') {
if (parentKey && PASSTHROUGH_KEYS.has(parentKey)) return body;
const stripped = stripHtml(body);
// Pentest 86.1/86.2 (LOW, 2026-06-19): Längen- + Whitelist-Check auf
// Kunden-/Vertrags-/Auftragsnummer-Feldern. validateContractIdentifier
// wirft ApiError(400) bei Verstoß → saubere 400-Antwort statt 500.
//
// Pentest 87.1 (LOW, 2026-06-19): Identifier-Felder MÜSSEN gegen den
// Raw-Input geprüft werden, NICHT gegen den stripHtml-Output. Sonst
// verschluckt der Sanitizer Tag-Verstöße still: `<b>bold</b>` würde
// als `"bold"` mit 200 OK durchgehen, `<script>alert(1)</script>`
// sogar zu `null` und damit den vorherigen Wert überschreiben.
// Die strikte Whitelist (`^[A-Za-z0-9_\-/. ]{0,100}$`) deckt alle
// Bypässe ab, die stripHtml normalerweise auffangen würde
// (Tags, Schemes, Zero-Width-Chars, Homoglyphe, Percent-Encoding)
// sie sind alle nicht in der Allowlist und fliegen mit 400 raus.
if (parentKey && isContractIdentifierField(parentKey)) {
return validateContractIdentifier(stripped, parentKey);
return validateContractIdentifier(body, parentKey);
}
return stripped;
return stripHtml(body);
}
if (Array.isArray(body)) return body.map((v) => sanitizeContractBody(v, parentKey));
if (typeof body === 'object') {