Security-Hardening Runde 15: Pentest Runde 12 Folge-Fixes
M2-Reste – XSS-Strings + Mass-Assignment-Settings noch in DB:
Idempotentes Cleanup-Script prisma/cleanup-xss-and-mass-assignment.ts.
Strippt HTML aus Customer/User-String-Feldern, entfernt AppSettings
ohne Whitelist-Eintrag. Wird im entrypoint.sh nach Migrations + Seed
einmalig pro Container-Start ausgeführt.
User-Update + password-Feld:
password aus USER_UPDATABLE_FIELDS raus (CREATE behält es), neuer
dedizierter Endpoint POST /api/users/:id/password mit Audit-Log
"Passwort … durch Admin gesetzt" und Komplexitäts-Check.
JS-Runtime-Fehler-Leak:
ORM_LEAK_PATTERNS um TypeError/ReferenceError/SyntaxError/RangeError +
"Cannot read properties of undefined/null" + "is not a function/
defined" erweitert. Greift im globalen res.json()-Wrapper.
POST /contracts substring-Crash:
Controller validiert type/customerId, sonst 400. generateContractNumber
fängt nullish type ab (Fallback "CON").
Seed-Admin-Passwort:
Default "admin" verletzte 12-Zeichen-Policy. Jetzt 16-char
Zufallspasswort (alle 4 Klassen garantiert via Fisher-Yates) oder per
SEED_ADMIN_PASSWORD-ENV überschreibbar. BCRYPT-Cost 12 (war 10).
Passwort wird einmalig in stdout ausgegeben mit Warnung.
AppSettings-Whitelist: companyName + defaultEmailDomain ergänzt
(kamen aus seed.ts, in 1. Whitelist vergessen).
Live-verifiziert:
- POST /contracts {} → 400 "Vertrags-Typ erforderlich" (vorher
TypeError-Stack)
- PUT /users/6 {password:"HackerPW2026!"} → 200 aber Login mit altem
PW geht weiter
- POST /users/6/password mit "kurz" → 400 mit Komplexitäts-Fehlern
- Cleanup-Script: planted XSS bereinigt, hackerSetting+debugMode
entfernt, idempotenter Re-Lauf
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* 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(', ') + ')' : ''}`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('=== Cleanup: XSS-Reste + Mass-Assignment-AppSettings ===');
|
||||
await cleanupXss();
|
||||
await cleanupAppSettings();
|
||||
console.log('=== Fertig. ===');
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error('Cleanup fehlgeschlagen:', e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
+41
-3
@@ -221,8 +221,37 @@ async function main() {
|
||||
|
||||
console.log('Roles created');
|
||||
|
||||
// Create admin user
|
||||
const hashedPassword = await bcrypt.hash('admin', 10);
|
||||
// Admin-User anlegen. Standard-Passwort darf NIEMALS in der Source-Repo
|
||||
// landen (Pentest Runde 12: "admin" verletzt die eigene 12-Zeichen-
|
||||
// Komplexitätspolicy). Stattdessen:
|
||||
// - SEED_ADMIN_PASSWORD-ENV → wird verwendet (z.B. via docker-compose env)
|
||||
// - sonst → zufälliges 16-Zeichen-Passwort, wird ein einziges Mal beim
|
||||
// Seed in stdout ausgegeben. Wer das Log nicht sieht, muss
|
||||
// Passwort-vergessen-Flow nutzen.
|
||||
// Hash-Cost: 12 (OWASP 2026), nicht mehr 10.
|
||||
function generateInitialPassword(): string {
|
||||
const upper = 'ABCDEFGHJKLMNPQRSTUVWXYZ';
|
||||
const lower = 'abcdefghijkmnopqrstuvwxyz';
|
||||
const digits = '23456789';
|
||||
const special = '!@#$%&*+=?';
|
||||
const all = upper + lower + digits + special;
|
||||
const pick = (s: string) => s[Math.floor(Math.random() * s.length)];
|
||||
// mind. einen aus jeder Klasse + Rest zufällig
|
||||
const chars = [pick(upper), pick(lower), pick(digits), pick(special)];
|
||||
for (let i = chars.length; i < 16; i++) chars.push(pick(all));
|
||||
// Fisher-Yates Shuffle (sonst stehen die garantierten Klassen-Zeichen am Anfang)
|
||||
for (let i = chars.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[chars[i], chars[j]] = [chars[j], chars[i]];
|
||||
}
|
||||
return chars.join('');
|
||||
}
|
||||
|
||||
const envPassword = process.env.SEED_ADMIN_PASSWORD;
|
||||
const adminPlainPassword = envPassword && envPassword.length >= 12
|
||||
? envPassword
|
||||
: generateInitialPassword();
|
||||
const hashedPassword = await bcrypt.hash(adminPlainPassword, 12);
|
||||
|
||||
const adminUser = await prisma.user.upsert({
|
||||
where: { email: 'admin@admin.com' },
|
||||
@@ -238,7 +267,16 @@ async function main() {
|
||||
},
|
||||
});
|
||||
|
||||
console.log('Admin user created: admin@admin.com / admin');
|
||||
console.log('========================================================');
|
||||
console.log(' Admin-User: admin@admin.com');
|
||||
if (envPassword) {
|
||||
console.log(' Passwort: aus SEED_ADMIN_PASSWORD');
|
||||
} else {
|
||||
console.log(` Initial-Passwort: ${adminPlainPassword}`);
|
||||
console.log(' ⚠️ Dieses Passwort wird hier EINMAL ausgegeben!');
|
||||
console.log(' Bitte sofort nach dem ersten Login ändern.');
|
||||
}
|
||||
console.log('========================================================');
|
||||
|
||||
// Create some sales platforms
|
||||
const platforms = ['Moon Fachhandel', 'Verivox', 'Check24', 'Eigenvermittlung'];
|
||||
|
||||
Reference in New Issue
Block a user