diff --git a/backend/.env.example b/backend/.env.example index 631e3503..0bb9d15c 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -9,7 +9,9 @@ DATABASE_URL="mysql://user:password@localhost:3306/opencrm" # JWT JWT_SECRET="your-super-secret-jwt-key-change-in-production" -JWT_EXPIRES_IN="7d" +# Access kurz (XSS-Schutz, nur JS-Memory). Refresh lang im httpOnly-Cookie. +JWT_EXPIRES_IN="15m" +JWT_REFRESH_EXPIRES_IN="7d" # Encryption (for portal credentials) ENCRYPTION_KEY="32-byte-hex-key-for-aes-256-gcm" diff --git a/backend/prisma/cleanup-xss-and-mass-assignment.ts b/backend/prisma/cleanup-xss-and-mass-assignment.ts index 58b333b0..f4ce5631 100644 --- a/backend/prisma/cleanup-xss-and-mass-assignment.ts +++ b/backend/prisma/cleanup-xss-and-mass-assignment.ts @@ -75,10 +75,84 @@ async function cleanupAppSettings() { 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, + / + /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. ==='); } diff --git a/docker/.env.example b/docker/.env.example index 3c3c2042..4fe6d131 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -15,7 +15,10 @@ DB_PASSWORD=change-this-password # JWT Authentication # Generate with: openssl rand -base64 32 JWT_SECRET=change-this-to-a-secure-random-string -JWT_EXPIRES_IN=7d +# Access-Token kurz (XSS-Schutz, Token lebt nur im JS-Memory). +# Refresh-Token lang im httpOnly-Cookie. +JWT_EXPIRES_IN=15m +JWT_REFRESH_EXPIRES_IN=7d # Encryption Key (for portal credentials) # Generate with: openssl rand -hex 32 diff --git a/docs/todo.md b/docs/todo.md index e235822f..fd5ae7c5 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -97,6 +97,39 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung ## ✅ Erledigt +- [x] **🚨 Pentest Runde 17 – JWT-TTL + Pentest-Marker-Detection** + - **21.1 Access-Token 7 Tage**: Bug-Quelle waren die `.env`-Files, + die noch die alte Konvention vor der Refresh-Token-Trennung + hatten (`JWT_EXPIRES_IN=7d`). docker-compose.yml und + `.env.example` standen schon richtig auf 15m als Default. + Alle `.env`-Files (Root, backend/, docker/.env.example, + backend/.env.example) jetzt auf `JWT_EXPIRES_IN=15m` mit + explizitem `JWT_REFRESH_EXPIRES_IN=7d`. Auf prod kann der + Container mit dem neuen Default neu hochgezogen werden. + - **17.5 Alte Pentest-Daten in DB**: das Cleanup-Script läuft + schon bei jedem Container-Start, strippt HTML aus Customer/ + User-Strings und entfernt nicht-whitelisted AppSettings. Es + erkannte aber keine Test-Records ohne HTML (z.B. Customer mit + `email: hacker@evil.de`). Erweiterung: + * Neue Marker-Pattern-Liste: `^hacker@`, `^attacker@`, + `^pentest@`, `@evil\.`, `