Files
opencrm/backend/prisma/cleanup-xss-and-mass-assignment.ts
T
duffyduck 0f2dc44e45 cleanup: hacker@-Marker raus (legitime Nachnamen "Hacker")
Familie Hacker / Kunden mit "Hacker" als Nachnamen nutzen reichlich
hacker@familie-hacker.de & Co. Das `^hacker@`-Pattern hätte alle
fälschlich als Pentest-Marker erkannt. Raus damit.

Verbleibende Marker reichen aus:
- ^attacker@, ^pentest@, @evil.
- <script, onerror=, javascript:
- SQL-Injection-Pattern, Path-Traversal

Verifiziert: hacker@familie-hacker.de geht durch, attacker@evil.de
wird weiterhin erkannt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 20:50:36 +02:00

168 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Einmal-Bereinigung für Pentest-Reste (Runde 12 / 2026-05-18):
*
* 1. HTML-Tags aus Customer/User-Stringfeldern strippen (M2-Stored-XSS-Reste)
* 2. Unbekannte App-Settings entfernen, die durch Mass-Assignment in die DB
* gerutscht sind, BEVOR die Whitelist eingezogen wurde (M1-Reste).
*
* Idempotent: wenn nichts zu tun ist, ändert sich nichts. Bei Bedarf
* mehrfach aufrufbar.
*/
import prisma from '../src/lib/prisma.js';
import { stripHtml } from '../src/utils/sanitize.js';
import { ALLOWED_SETTING_KEYS } from '../src/services/appSetting.service.js';
const CUSTOMER_STRING_FIELDS = [
'salutation', 'firstName', 'lastName', 'companyName',
'birthPlace', 'email', 'phone', 'mobile',
'taxNumber', 'commercialRegisterNumber', 'notes',
];
const USER_STRING_FIELDS = [
'firstName', 'lastName', 'email',
'whatsappNumber', 'telegramUsername', 'signalNumber',
];
async function cleanupXss() {
const customers = await prisma.customer.findMany();
let touched = 0;
for (const c of customers) {
const updates: Record<string, string> = {};
for (const field of CUSTOMER_STRING_FIELDS) {
const v = (c as any)[field];
if (typeof v === 'string') {
const cleaned = stripHtml(v) as string;
if (cleaned !== v) updates[field] = cleaned;
}
}
if (Object.keys(updates).length > 0) {
console.log(` Customer #${c.id}: bereinigt:`, Object.keys(updates).join(', '));
await prisma.customer.update({ where: { id: c.id }, data: updates });
touched++;
}
}
console.log(` → Customer bereinigt: ${touched}`);
const users = await prisma.user.findMany();
let userTouched = 0;
for (const u of users) {
const updates: Record<string, string> = {};
for (const field of USER_STRING_FIELDS) {
const v = (u as any)[field];
if (typeof v === 'string') {
const cleaned = stripHtml(v) as string;
if (cleaned !== v) updates[field] = cleaned;
}
}
if (Object.keys(updates).length > 0) {
console.log(` User #${u.id}: bereinigt:`, Object.keys(updates).join(', '));
await prisma.user.update({ where: { id: u.id }, data: updates });
userTouched++;
}
}
console.log(` → User bereinigt: ${userTouched}`);
}
async function cleanupAppSettings() {
const settings = await prisma.appSetting.findMany();
const removed: string[] = [];
for (const s of settings) {
if (!ALLOWED_SETTING_KEYS.has(s.key)) {
removed.push(s.key);
await prisma.appSetting.delete({ where: { key: s.key } });
}
}
console.log(` → AppSettings entfernt: ${removed.length}${removed.length ? ' (' + removed.join(', ') + ')' : ''}`);
}
// Pattern, die auf typische Pentest-/Test-Daten hindeuten. Bewusst eng
// gefasst legitime Kunden mit "Hacker" als Nachnamen sollen nicht
// fälschlich getroffen werden (gibt's reichlich, gerade hier).
// Konkret weggelassen: `^hacker@` würde Verwandte/Kunden mit
// `hacker@familie-hacker.de` o.ä. fängen.
const PENTEST_MARKERS = [
/@evil\./i,
/^attacker@/i,
/^pentest@/i,
/<script\b/i, // unverwechselbarer XSS-Marker
/\bonerror\s*=/i, // <img onerror=…>
/javascript:/i, // javascript:-URL
/'\s*OR\s*'1'\s*=\s*'1/i, // SQL-Injection
/\.\.\/.*etc\/passwd/i, // Path-Traversal
];
function looksLikePentestData(value: unknown): boolean {
if (typeof value !== 'string') return false;
return PENTEST_MARKERS.some((re) => re.test(value));
}
async function findOrPurgePentestRecords() {
const purge = process.env.CLEANUP_PURGE_PENTEST === 'true';
const suspect: Array<{ kind: string; id: number; reason: string }> = [];
const customers = await prisma.customer.findMany();
for (const c of customers) {
for (const f of ['email', 'phone', 'mobile', 'firstName', 'lastName', 'companyName', 'notes']) {
if (looksLikePentestData((c as any)[f])) {
suspect.push({ kind: 'Customer', id: c.id, reason: `${f}=${JSON.stringify((c as any)[f]).slice(0, 60)}` });
break;
}
}
}
const users = await prisma.user.findMany();
for (const u of users) {
for (const f of ['email', 'firstName', 'lastName']) {
if (looksLikePentestData((u as any)[f])) {
suspect.push({ kind: 'User', id: u.id, reason: `${f}=${JSON.stringify((u as any)[f]).slice(0, 60)}` });
break;
}
}
}
if (suspect.length === 0) {
console.log(' → Keine Pentest-Marker in Customer/User-Records gefunden.');
return;
}
console.log(`${suspect.length} verdächtige Records (Pentest-Marker):`);
for (const s of suspect) {
console.log(` [${s.kind}#${s.id}] ${s.reason}`);
}
if (!purge) {
console.log(' ️ Zum Löschen Container mit CLEANUP_PURGE_PENTEST=true neu starten,');
console.log(' oder Records manuell über adminer entfernen.');
return;
}
for (const s of suspect) {
if (s.kind === 'Customer') {
await prisma.customer.delete({ where: { id: s.id } }).catch((e: any) => {
console.log(` [Customer#${s.id}] Löschen fehlgeschlagen: ${e.message?.slice(0, 80)}`);
});
} else if (s.kind === 'User') {
await prisma.user.delete({ where: { id: s.id } }).catch((e: any) => {
console.log(` [User#${s.id}] Löschen fehlgeschlagen: ${e.message?.slice(0, 80)}`);
});
}
}
console.log(`${suspect.length} verdächtige Records gelöscht.`);
}
async function main() {
console.log('=== Cleanup: XSS-Reste + Mass-Assignment-AppSettings ===');
await cleanupXss();
await cleanupAppSettings();
await findOrPurgePentestRecords();
console.log('=== Fertig. ===');
}
main()
.catch((e) => {
console.error('Cleanup fehlgeschlagen:', e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});