fix: IBANs im Logbuch maskieren

This commit is contained in:
2026-09-09 13:04:13 +02:00
parent 4e05906556
commit bb18a974dd
4 changed files with 113 additions and 11 deletions

View File

@@ -2,6 +2,7 @@
import { FastifyInstance } from "fastify"; import { FastifyInstance } from "fastify";
import { and, asc, eq, inArray } from "drizzle-orm"; import { and, asc, eq, inArray } from "drizzle-orm";
import { authProfiles, historyitems } from "../../db/schema"; import { authProfiles, historyitems } from "../../db/schema";
import { sanitizeHistoryItem, sanitizeHistoryText, sanitizeHistoryValue } from "../utils/historySanitization";
const columnMap: Record<string, any> = { const columnMap: Record<string, any> = {
customers: historyitems.customer, customers: historyitems.customer,
@@ -95,7 +96,7 @@ export default async function resourceHistoryRoutes(server: FastifyInstance) {
profiles.map((profile) => [profile.user_id, profile]) profiles.map((profile) => [profile.user_id, profile])
); );
return data.map((historyitem) => ({ return data.map((historyitem) => sanitizeHistoryItem({
...historyitem, ...historyitem,
created_at: historyitem.createdAt, created_at: historyitem.createdAt,
created_by: historyitem.createdBy, created_by: historyitem.createdBy,
@@ -153,7 +154,7 @@ export default async function resourceHistoryRoutes(server: FastifyInstance) {
profiles.map((profile) => [profile.user_id, profile]) profiles.map((profile) => [profile.user_id, profile])
) )
const dataCombined = data.map((historyitem) => ({ const dataCombined = data.map((historyitem) => sanitizeHistoryItem({
...historyitem, ...historyitem,
created_at: historyitem.createdAt, created_at: historyitem.createdAt,
created_by: historyitem.createdBy, created_by: historyitem.createdBy,
@@ -223,11 +224,11 @@ export default async function resourceHistoryRoutes(server: FastifyInstance) {
const inserted = await server.db const inserted = await server.db
.insert(historyitems) .insert(historyitems)
.values({ .values({
text, text: sanitizeHistoryText(text),
[fkField]: parseId(id), [fkField]: parseId(id),
oldVal: old_val || null, oldVal: sanitizeHistoryValue(old_val) || null,
newVal: new_val || null, newVal: sanitizeHistoryValue(new_val) || null,
config: config || null, config: sanitizeHistoryValue(config) || null,
tenant: (req.user as any)?.tenant_id, tenant: (req.user as any)?.tenant_id,
createdBy: userId createdBy: userId
}) })
@@ -238,10 +239,10 @@ export default async function resourceHistoryRoutes(server: FastifyInstance) {
return reply.code(500).send({ error: "Failed to create history entry" }); return reply.code(500).send({ error: "Failed to create history entry" });
} }
return reply.code(201).send({ return reply.code(201).send(sanitizeHistoryItem({
...data, ...data,
created_at: data.createdAt, created_at: data.createdAt,
created_by: data.createdBy created_by: data.createdBy
}); }));
}); });
} }

View File

@@ -1,5 +1,6 @@
import { FastifyInstance } from "fastify" import { FastifyInstance } from "fastify"
import { historyitems } from "../../db/schema"; import { historyitems } from "../../db/schema";
import { sanitizeHistoryText, sanitizeHistoryValue } from "./historySanitization";
const HISTORY_ENTITY_LABELS: Record<string, string> = { const HISTORY_ENTITY_LABELS: Record<string, string> = {
customers: "Kunden", customers: "Kunden",
@@ -114,11 +115,11 @@ export async function insertHistoryItem(
const entry = { const entry = {
tenant: params.tenant_id, tenant: params.tenant_id,
createdBy: params.created_by, createdBy: params.created_by,
text: params.text || textMap[params.action], text: sanitizeHistoryText(params.text || textMap[params.action]),
action: params.action, action: params.action,
[fkColumn]: params.entityId, [fkColumn]: params.entityId,
oldVal: stringifyHistoryValue(params.oldVal), oldVal: stringifyHistoryValue(sanitizeHistoryValue(params.oldVal)),
newVal: stringifyHistoryValue(params.newVal) newVal: stringifyHistoryValue(sanitizeHistoryValue(params.newVal))
} }
await server.db.insert(historyitems).values(entry as any) await server.db.insert(historyitems).values(entry as any)

View File

@@ -0,0 +1,45 @@
const IBAN_IN_TEXT_PATTERN = /\b([A-Z]{2}\d{2}(?:[\s-]?[A-Z0-9]){11,30})(?=["'\]},.;:!?)]|$)/gi
export function maskIban(iban: string): string {
const normalized = iban.replace(/[\s-]+/g, "").toUpperCase()
if (normalized.length <= 8) return normalized
return `${normalized.slice(0, 4)} **** **** ${normalized.slice(-4)}`
}
export function sanitizeHistoryText(text: string): string {
return text.replace(IBAN_IN_TEXT_PATTERN, (candidate) => maskIban(candidate))
}
function sanitizeIbanField(value: any): any {
if (typeof value === "string") return maskIban(value)
if (Array.isArray(value)) return value.map(sanitizeIbanField)
return sanitizeHistoryValue(value)
}
export function sanitizeHistoryValue(value: any): any {
if (typeof value === "string") return sanitizeHistoryText(value)
if (Array.isArray(value)) return value.map(sanitizeHistoryValue)
if (!value || typeof value !== "object" || value instanceof Date) return value
const prototype = Object.getPrototypeOf(value)
if (prototype !== Object.prototype && prototype !== null) return value
return Object.fromEntries(
Object.entries(value).map(([key, nestedValue]) => [
key,
key.toLowerCase().includes("iban")
? sanitizeIbanField(nestedValue)
: sanitizeHistoryValue(nestedValue),
])
)
}
export function sanitizeHistoryItem<T extends Record<string, any>>(item: T): T {
return {
...item,
text: typeof item.text === "string" ? sanitizeHistoryText(item.text) : item.text,
oldVal: sanitizeHistoryValue(item.oldVal),
newVal: sanitizeHistoryValue(item.newVal),
config: sanitizeHistoryValue(item.config),
}
}

View File

@@ -0,0 +1,55 @@
import test from "node:test"
import assert from "node:assert/strict"
import {
maskIban,
sanitizeHistoryItem,
sanitizeHistoryText,
sanitizeHistoryValue,
} from "../src/utils/historySanitization"
const IBAN = "DE89370400440532013000"
test("masks all but the first and last four IBAN characters", () => {
assert.equal(maskIban(IBAN), "DE89 **** **** 3000")
assert.equal(maskIban("DE89 3704 0044 0532 0130 00"), "DE89 **** **** 3000")
})
test("masks IBAN values in nested history data", () => {
const sanitized = sanitizeHistoryValue({
name: "Beispielkunde",
infoData: {
bankingIban: IBAN,
bankingIbans: [IBAN, "AT61 1904 3002 3457 3201"],
},
})
assert.deepEqual(sanitized, {
name: "Beispielkunde",
infoData: {
bankingIban: "DE89 **** **** 3000",
bankingIbans: ["DE89 **** **** 3000", "AT61 **** **** 3201"],
},
})
})
test("masks IBANs embedded in generated history text", () => {
const text = `Kunden: Info Daten geändert von "{\"bankingIbans\":[\"${IBAN}\"]}"`
const sanitized = sanitizeHistoryText(text)
assert.equal(sanitized.includes(IBAN), false)
assert.equal(sanitized.includes("DE89 **** **** 3000"), true)
})
test("sanitizes existing history items before they are returned", () => {
const sanitized = sanitizeHistoryItem({
text: `IBAN: ${IBAN}.`,
oldVal: JSON.stringify({ bankingIban: IBAN }),
newVal: { iban: IBAN },
config: null,
})
assert.equal(sanitized.text, "IBAN: DE89 **** **** 3000.")
assert.equal(sanitized.oldVal.includes(IBAN), false)
assert.deepEqual(sanitized.newVal, { iban: "DE89 **** **** 3000" })
})