diff --git a/apps/api/src/modules/invoices/index.ts b/apps/api/src/modules/invoices/index.ts index 9adb18f..7d634a5 100644 --- a/apps/api/src/modules/invoices/index.ts +++ b/apps/api/src/modules/invoices/index.ts @@ -93,7 +93,10 @@ function exportFilter(q: z.infer): { sql: string; params: un return { sql: conds.join(' AND '), params }; } const csvNum = (cents: number) => (cents / 100).toFixed(2).replace('.', ','); -const csvCell = (s: string): string => (/[;"\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s); +// Formel-Injection-Schutz (CSV): Zellen, die mit =, +, -, @ oder Tab beginnen, werden von Excel/Sheets sonst als Formel +// ausgeführt. Firmenname/Notiz stammen aus vom Kunden bzw. Personal eingegebenen Feldern – daher immer entschärfen. +const csvSafe = (s: string): string => (/^[=+\-@\t]/.test(s) ? `'${s}` : s); +const csvCell = (s: string): string => { const v = csvSafe(s); return /[;"\n]/.test(v) ? `"${v.replace(/"/g, '""')}"` : v; }; const csvDate = (d: unknown) => (d ? new Date(d as string).toISOString().slice(0, 10) : ''); const STATUS_LABEL: Record = { open: 'Offen', paid: 'Bezahlt', cancelled: 'Storniert' }; @@ -123,7 +126,7 @@ const DATEV_BOOKING_HEADER = [ const pad2 = (n: number) => String(n).padStart(2, '0'); const datevYmd = (d: Date) => `${d.getUTCFullYear()}${pad2(d.getUTCMonth() + 1)}${pad2(d.getUTCDate())}`; const datevDm = (d: Date) => `${pad2(d.getUTCDate())}${pad2(d.getUTCMonth() + 1)}`; -const datevQ = (s: string) => `"${s.replace(/"/g, '""')}"`; +const datevQ = (s: string) => `"${csvSafe(s).replace(/"/g, '""')}"`; const datevNum = (cents: number) => (cents / 100).toFixed(2).replace('.', ','); /** Eine Buchungszeile mit genau 125 Feldern; nur die übergebenen (1-basierten) Spaltennummern werden gesetzt, der Rest bleibt leer. */ function datevRow(vals: Record): string {