From 955fceb3b8f5b68dc6c4863923fd85f8d34684ca Mon Sep 17 00:00:00 2001 From: duffyduck Date: Wed, 12 Aug 2026 14:46:56 +0200 Subject: [PATCH] BLZ-Guard: vollstaendige Formatpruefung aller Eintraege (Pentest R148) Der Poisoning-Guard pruefte bisher nur die erste current-Zeile - ein Set mit 1 echten + 999 Fake-Eintraegen kaeme durch. Jetzt wird JEDER Eintrag in current + next.upsert geprueft (8-stellige BLZ, Wert [Name] oder [Name,BIC]), next.remove auf 8-stellige BLZ, next.valid auf ein gueltiges Datum. Dabei korrekt beruecksichtigt: Banken ohne BIC haben nur [Name] (Laenge 1) - lookupBlz liefert dann bic:''. Real gegen den echten Datensatz verifiziert (3506 Eintraege, inkl. BIC-lose wie BLZ 60050009). Co-Authored-By: Claude Opus 4.8 --- backend/src/services/blzData.service.ts | 47 +++++++++++++++++++++---- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/backend/src/services/blzData.service.ts b/backend/src/services/blzData.service.ts index d2a31e00..3702db9a 100644 --- a/backend/src/services/blzData.service.ts +++ b/backend/src/services/blzData.service.ts @@ -35,7 +35,7 @@ const REGISTRY_BASE = process.env.BLZ_REGISTRY_BASE || 'https://registry.npmjs.o const FETCH_TIMEOUT_MS = 20_000; // ---- Typen ---- -type BankTuple = [string, string]; // [bankName, bic] +type BankTuple = [string, string?]; // [bankName, bic?] – BIC fehlt bei manchen Banken type CurrentData = Record; interface NextData { valid: string; @@ -159,7 +159,7 @@ export function lookupBlz(blz: string): BankInfo | null { if (!/^[1-9]\d{7}$/.test(blz)) return null; const entry = combinedData()[blz]; if (!entry) return null; - return { bankName: entry[0], bic: entry[1], blz }; + return { bankName: entry[0], bic: entry[1] ?? '', blz }; } /** Extrahiert die BLZ aus einer deutschen IBAN und schlägt sie nach. */ @@ -171,20 +171,53 @@ export function lookupByIban(iban: string): BankInfo | null { // ---- Validierung des Roh-Datensatzes (gegen Müll/HTML-Antworten) ---- +// Prüft eine BLZ→[Name,BIC]-Map vollständig: JEDER Schlüssel eine 8-stellige +// BLZ, JEDER Wert genau [string, string] (BIC darf leer sein). Nicht nur die +// erste Zeile (Pentest R148, Code-Note) – sonst käme ein Set mit 1 echten + +// 999 Fake-Einträgen durch. +function assertValidBankMap(map: Record, label: string): void { + for (const [blz, entry] of Object.entries(map)) { + if (!/^\d{8}$/.test(blz)) { + throw new Error(`${label}: ungültige BLZ "${blz}"`); + } + // Wert ist [Name] (Bank ohne BIC) ODER [Name, BIC]. Beides ist in der + // echten Bundesbank-Datei vorhanden (z.B. reine Zahlungsverkehr-BLZ). + if ( + !Array.isArray(entry) || + entry.length < 1 || + entry.length > 2 || + typeof entry[0] !== 'string' || + (entry.length === 2 && typeof entry[1] !== 'string') + ) { + throw new Error(`${label}: Eintrag ${blz} hat unerwartetes Format`); + } + } +} + function assertValidRawDataset(raw: any): asserts raw is RawDataset { if (!raw || typeof raw !== 'object') throw new Error('Datensatz ist kein Objekt'); const cur = raw.current; if (!cur || typeof cur !== 'object' || Array.isArray(cur)) throw new Error('current fehlt/ungültig'); const keys = Object.keys(cur); if (keys.length < 1000) throw new Error(`current zu klein (${keys.length} Einträge)`); - const sample = cur[keys[0]]; - if (!Array.isArray(sample) || sample.length !== 2 || typeof sample[0] !== 'string' || typeof sample[1] !== 'string') { - throw new Error('current-Eintrag hat unerwartetes Format'); - } + // VOLLSTÄNDIGE Formatprüfung aller Einträge, nicht nur des ersten. + assertValidBankMap(cur, 'current'); + const next = raw.next; - if (!next || typeof next !== 'object' || typeof next.valid !== 'string' || typeof next.upsert !== 'object' || !Array.isArray(next.remove)) { + if (!next || typeof next !== 'object' || typeof next.valid !== 'string' || typeof next.upsert !== 'object' || Array.isArray(next.upsert) || !Array.isArray(next.remove)) { throw new Error('next fehlt/ungültig'); } + // Datum plausibel? + if (Number.isNaN(new Date(next.valid).getTime())) { + throw new Error('next.valid ist kein gültiges Datum'); + } + // upsert-Einträge ebenso streng; remove muss aus 8-stelligen BLZ bestehen. + assertValidBankMap(next.upsert, 'next.upsert'); + for (const blz of next.remove) { + if (typeof blz !== 'string' || !/^\d{8}$/.test(blz)) { + throw new Error(`next.remove enthält ungültige BLZ "${blz}"`); + } + } } // ---- Ausgehende Requests (nur Updater) ----