Pentest R89: Provider-Adressfelder härten
R89.1 MEDIUM + R89.2 LOW: sanitizeNotes(…, 500) macht silent slice(0, 500) statt 400, und stripHtml lief vor dem Length- Check – `<script>…</script>` reduzierte auf "" → null in DB → vorheriger Wert silent überschrieben (R87.1-Pattern auf Adress-Feldern). Fix: validateProviderAddress() in sanitize.ts – Raw-Input, max 500 mit ApiError(400), Blacklist <, >, Tab + alle Control-Chars außer \n. CRLF → LF VOR dem Length-Check, damit Editoren mit \r\n-Line-Endings nicht doppelt zählen. Eingehängt in stripProviderStrings für contactAddress/cancellationAddress. R89.3/R89.4 (Quotes/\n) bewusst akzeptiert – Pentester selbst sagt "kein Risiko", sind in Adressen legitim. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -396,6 +396,53 @@ export function validateContractIdentifier(
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
// Pentest 89.1 + 89.2 (MEDIUM/LOW, 2026-06-21): Postadressen am
|
||||
// Provider (`contactAddress`, `cancellationAddress`). sanitizeNotes
|
||||
// hat das Length-Cap silent durchgeschoben (slice statt Error) und
|
||||
// stripHtml lief vor dem Length-Check – derselbe Fehler wie R87:
|
||||
// `<script>…</script>` reduziert auf leeren String → null in der
|
||||
// DB → vorheriger Wert ohne Fehlermeldung überschrieben.
|
||||
//
|
||||
// Lösung wie R87: Raw-Input validieren, harte 400 statt silent-Mutation.
|
||||
// - max 500 Zeichen (mehrzeilige Postadresse ≈ 4 Zeilen × 80)
|
||||
// - `<` oder `>` direkt 400 (Postadressen brauchen kein HTML)
|
||||
// - Steuerzeichen außer `\n` direkt 400 (kein CR/Tab/Null/etc)
|
||||
// - leerer/getrimmt-leerer Input → null (Feld zurücksetzen)
|
||||
// - CRLF → LF normalisieren
|
||||
const PROVIDER_ADDRESS_MAX_LEN = 500;
|
||||
// Erlaubt: nur LF (`\x0A`) als Newline. Alles andere – inkl. Tab (`\x09`) –
|
||||
// fliegt raus. Tab in Postadressen ist Header-Injection-Vektor für CSV/Mail
|
||||
// und nichts, was ein Mensch je tippt.
|
||||
const PROVIDER_ADDRESS_BAD_CHARS = /[\x00-\x09\x0B\x0C\x0E-\x1F\x7F<>]/;
|
||||
|
||||
export function validateProviderAddress(
|
||||
raw: unknown,
|
||||
fieldLabel: string,
|
||||
): string | null {
|
||||
if (raw == null) return null;
|
||||
if (typeof raw !== 'string') {
|
||||
throw new ApiError(400, `${fieldLabel} muss ein Text sein.`);
|
||||
}
|
||||
// CRLF → LF NORMALISIEREN bevor wir auf Länge prüfen – ein Editor der
|
||||
// immer `\r\n` schickt würde sonst bei jedem Zeilenumbruch zwei
|
||||
// Zeichen gegen das 500er-Cap zählen.
|
||||
const normalized = raw.replace(/\r\n?/g, '\n');
|
||||
if (PROVIDER_ADDRESS_BAD_CHARS.test(normalized)) {
|
||||
throw new ApiError(
|
||||
400,
|
||||
`${fieldLabel} enthält unzulässige Zeichen (HTML, Tabs oder Steuerzeichen sind in Postadressen nicht erlaubt).`,
|
||||
);
|
||||
}
|
||||
if (normalized.length > PROVIDER_ADDRESS_MAX_LEN) {
|
||||
throw new ApiError(
|
||||
400,
|
||||
`${fieldLabel} darf maximal ${PROVIDER_ADDRESS_MAX_LEN} Zeichen lang sein.`,
|
||||
);
|
||||
}
|
||||
const trimmed = normalized.trim();
|
||||
return trimmed === '' ? null : trimmed;
|
||||
}
|
||||
|
||||
const NOTES_DEFAULT_MAX = 2000;
|
||||
export function sanitizeNotes(raw: unknown, maxLength: number = NOTES_DEFAULT_MAX): string | null {
|
||||
if (raw == null) return null;
|
||||
|
||||
Reference in New Issue
Block a user