48fe69cdab
Pentest Runde 17: 21.1 Access-Token TTL war 7 Tage statt 15min: docker-compose.yml und .env.example standen schon richtig auf 15m als Default. Die alten Beispiel-.env-Files (backend/.env.example, docker/.env.example) hatten noch die alte Konvention "7d". Beide auf 15m korrigiert + explizites JWT_REFRESH_EXPIRES_IN=7d ergänzt. Auf prod muss die echte .env entsprechend angepasst werden. 17.5 Alte Pentest-Daten in DB: Cleanup-Script erweitert um Pentest-Marker-Erkennung: - Email-Pattern: ^hacker@, ^attacker@, ^pentest@, @evil\. - XSS-Marker: <script, onerror=, javascript: - Sonstige: SQL-Injection, Path-Traversal Bewusst eng gefasst (Marker MUSS am Email-Anfang stehen), damit legitime Kunden wie "stefanhacker@gmx.de" nicht als Pentest-Daten durchgehen. Default: nur warnen + Records auflisten. Opt-In via CLEANUP_PURGE_PENTEST=true löscht die markierten Customer/User. Live-verifiziert: - stefanhacker@gmx.de (echt) → durchgelassen - hacker@evil.de (Pentest) → erkannt + Warnung - Mit Purge-Env → gelöscht 18.4 Klartext-Portal-PW-Abruf: Bewusst drin gelassen (Admin-UI-Komfort). Endpoint ist mit customers:update-Permission gated + Audit-Log (READ → PortalPassword) – kein Bypass-Risiko, nur explizite Audit-Pflicht. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
167 lines
5.6 KiB
TypeScript
167 lines
5.6 KiB
TypeScript
/**
|
||
* 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, damit legitime Kunden mit "hacker" o.ä. im Nachnamen NICHT
|
||
// als Pentest-Marker durchgehen ("stefanhacker@gmx.de" ist echt).
|
||
const PENTEST_MARKERS = [
|
||
/@evil\./i,
|
||
/^hacker@/i, // Email beginnt mit "hacker@" – nicht im Mittelteil
|
||
/^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();
|
||
});
|