Pentest 58.1 MEDIUM: documentType jetzt mit echter Whitelist-Validierung

Bisher lief documentType nur durch stripHtml – ein beliebiger String
("NICHT_ERLAUBT", "DROP TABLE ...", Tippfehler) wurde 1:1 als
ContractDocument.documentType in die DB geschrieben. Das brach
Frontend-Filter, Lieferbestätigung-Auto-Activation und Reports.

Neuer validateContractDocumentType-Helper in utils/sanitize:
- Whitelist ALLOWED_CONTRACT_DOCUMENT_TYPES (8 Werte, gespiegelt aus
  Frontend CONTRACT_DOCUMENT_TYPES)
- Case-insensitiver Match, Rückgabe ist immer der kanonische Wert
- Wirft sprechende 400-Fehlermeldung mit Liste der erlaubten Werte

Eingesetzt in allen 3 Schreibpfaden:
- contract.controller.uploadContractDocument (multer-Datei wird bei
  Reject sauber gelöscht)
- cachedEmail.controller.saveEmailAsContractDocument
- cachedEmail.controller.saveAttachmentAsContractDocument

Audit-Log + maybeActivateOnDeliveryConfirmation nutzen jetzt den
kanonischen Wert (statt der rohen Eingabe), damit Reports
einheitlich aussehen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-01 21:53:34 +02:00
parent 9482424ade
commit 6b1d493f0b
3 changed files with 83 additions and 20 deletions
+47
View File
@@ -178,6 +178,53 @@ export function sanitizeCustomers<T extends Record<string, unknown>>(customers:
// - Tags + gefährliche Schemata via stripHtml
// - CRLF auf Newline normalisieren, keine Carriage-Returns persistieren
// - Length-Cap default 2000 Zeichen (genug für sinnvolle Anmerkungen)
// Pentest 58.1 (MEDIUM, 2026-06-01): documentType wurde nur durch
// stripHtml geschickt, aber NICHT gegen eine Whitelist geprüft. Damit
// landeten beliebige Strings (`NICHT_ERLAUBT`, `DROP TABLE …`,
// Tippfehler-Werte aus alten UI-Versionen) als documentType in der
// ContractDocument-Tabelle und brachen Frontend-Filter, Auto-Activation
// (Lieferbestätigung-Trigger) und Reports.
//
// Whitelist spiegelt die Konstante CONTRACT_DOCUMENT_TYPES aus
// SaveAttachmentModal / SaveEmailAsPdfModal im Frontend. Beide
// Listen MÜSSEN synchron gehalten werden idealerweise später
// in eine geteilte Konfiguration gehoben.
export const ALLOWED_CONTRACT_DOCUMENT_TYPES = [
'Auftragsformular',
'Auftragsbestätigung',
'Lieferbestätigung',
'Vertragsunterlagen',
'Vollmacht',
'Widerrufsbelehrung',
'Preisblatt',
'Sonstiges',
] as const;
const CONTRACT_DOCUMENT_TYPE_SET: Set<string> = new Set(ALLOWED_CONTRACT_DOCUMENT_TYPES);
/**
* Validiert + normalisiert einen documentType-Wert. Wirft einen Fehler
* mit klarer Liste, wenn der Wert nicht in der Whitelist steht (der
* aufrufende Controller mappt das auf 400). Trimmt Whitespace und macht
* den Vergleich case-insensitive damit `"lieferbestätigung"` aus
* Drittsystemen sauber matched, aber `"Lieferbestätigung_DROP"` rausfliegt.
*/
export function validateContractDocumentType(raw: unknown): string {
if (typeof raw !== 'string') {
throw new Error(`documentType ist erforderlich. Erlaubt: ${ALLOWED_CONTRACT_DOCUMENT_TYPES.join(', ')}`);
}
const cleaned = stripHtml(raw) as string;
const trimmed = cleaned.trim();
if (trimmed === '') {
throw new Error(`documentType ist erforderlich. Erlaubt: ${ALLOWED_CONTRACT_DOCUMENT_TYPES.join(', ')}`);
}
const canonical = ALLOWED_CONTRACT_DOCUMENT_TYPES.find((t) => t.toLowerCase() === trimmed.toLowerCase());
if (!canonical) {
throw new Error(`Ungültiger documentType '${trimmed}'. Erlaubt: ${ALLOWED_CONTRACT_DOCUMENT_TYPES.join(', ')}`);
}
return canonical;
}
const NOTES_DEFAULT_MAX = 2000;
export function sanitizeNotes(raw: unknown, maxLength: number = NOTES_DEFAULT_MAX): string | null {
if (raw == null) return null;