From a7d12b854017ccda4cde3cf9198e0fbc64ea1993 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Sat, 16 May 2026 19:39:02 +0200 Subject: [PATCH] Security-Hardening Runde 7: Pentest Runde 3 (3 Findings) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit KRITISCH – Privilege Escalation: POST /api/developer/setup war ohne Auth erreichbar und konnte developer:access der Admin-Rolle hinzufügen → volle DB-Kontrolle via /developer/*-Routen. Endpoint ersatzlos entfernt; manuelles Setzen geht über prisma/add-developer-permission.ts (CLI). HOCH – Fehlende Migration auf Prod: portalPasswordMustChange war im Code, aber prod-DB hatte die Spalte nicht → jeder Kunden-Login warf Prisma-Schema-Error → DoS. Root Cause: db push statt migrate dev während Entwicklung → kein Migration-File im Repo. Fix: handgenerierte Migration 20260516173552_portal_password_must_change/migration.sql, lokal mit migrate resolve --applied registriert, durch shadow-DB-Reset verifiziert. entrypoint.sh führt migrate deploy bereits aus. MITTEL – Prisma-Internals-Leak im Login-Error: error.message wurde 1:1 an den Client gegeben → bei DB-Schema- Fehlern leakten Tabellen- und Spaltennamen. Whitelist-Filter safeLoginError() in auth.controller.ts: nur 'Ungültige Anmeldedaten' und 'E-Mail und Passwort erforderlich' werden durchgereicht, alles andere wird zu generischem 'Anmeldung fehlgeschlagen' maskiert. Original landet im Server-Log. Live-verifiziert: - POST /api/developer/setup → HTTP 404 - Falsches Customer-PW → 'Ungültige Anmeldedaten' (keine Internals) - Spalte testweise gedropped → 'Anmeldung fehlgeschlagen' (generisch), Original-Message nur im Server-Log - Shadow-DB-Reset + migrate deploy → Spalte korrekt erzeugt Co-Authored-By: Claude Opus 4.7 (1M context) --- .../migration.sql | 2 + backend/src/controllers/auth.controller.ts | 24 ++++++++- backend/src/routes/developer.routes.ts | 49 ++----------------- docs/todo.md | 31 ++++++++++++ 4 files changed, 60 insertions(+), 46 deletions(-) create mode 100644 backend/prisma/migrations/20260516173552_portal_password_must_change/migration.sql diff --git a/backend/prisma/migrations/20260516173552_portal_password_must_change/migration.sql b/backend/prisma/migrations/20260516173552_portal_password_must_change/migration.sql new file mode 100644 index 00000000..0961b0c9 --- /dev/null +++ b/backend/prisma/migrations/20260516173552_portal_password_must_change/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE `Customer` ADD COLUMN `portalPasswordMustChange` BOOLEAN NOT NULL DEFAULT false; diff --git a/backend/src/controllers/auth.controller.ts b/backend/src/controllers/auth.controller.ts index 64a57f95..b5a6ecff 100644 --- a/backend/src/controllers/auth.controller.ts +++ b/backend/src/controllers/auth.controller.ts @@ -27,6 +27,26 @@ function clearRefreshCookie(res: Response): void { res.clearCookie(REFRESH_COOKIE_NAME, { path: '/api/auth' }); } +// Whitelist von Fehlermeldungen, die wir an Login-Clients durchreichen dürfen. +// ALLES andere (Prisma-Internals, DB-Connection-Errors, Schema-Fehler, ...) +// wird als generisches "Anmeldung fehlgeschlagen" maskiert – die Original- +// Message bleibt im Server-Log, leakt aber nicht im HTTP-Response. Pentest +// Runde 3 (2026-05-16): `prisma.customer.findUnique() invocation: The column +// X does not exist` war im Body sichtbar → Tabellen-/Spaltennamen geleakt. +const SAFE_LOGIN_ERRORS = new Set([ + 'Ungültige Anmeldedaten', + 'E-Mail und Passwort erforderlich', +]); +function safeLoginError(err: unknown): string { + if (err instanceof Error && SAFE_LOGIN_ERRORS.has(err.message)) { + return err.message; + } + if (err instanceof Error) { + console.error('[Login] Unerwarteter Fehler (maskiert):', err.message); + } + return 'Anmeldung fehlgeschlagen'; +} + // Mitarbeiter-Login export async function login(req: Request, res: Response): Promise { const { email, password } = req.body || {}; @@ -68,7 +88,7 @@ export async function login(req: Request, res: Response): Promise { }); res.status(401).json({ success: false, - error: error instanceof Error ? error.message : 'Anmeldung fehlgeschlagen', + error: safeLoginError(error), } as ApiResponse); } } @@ -112,7 +132,7 @@ export async function customerLogin(req: Request, res: Response): Promise }); res.status(401).json({ success: false, - error: error instanceof Error ? error.message : 'Anmeldung fehlgeschlagen', + error: safeLoginError(error), } as ApiResponse); } } diff --git a/backend/src/routes/developer.routes.ts b/backend/src/routes/developer.routes.ts index 8518c0c6..56e6f171 100644 --- a/backend/src/routes/developer.routes.ts +++ b/backend/src/routes/developer.routes.ts @@ -1,54 +1,15 @@ import { Router, Response } from 'express'; -import { Prisma } from '@prisma/client'; import prisma from '../lib/prisma.js'; import { authenticate, requirePermission } from '../middleware/auth.js'; import { AuthRequest } from '../types/index.js'; const router = Router(); -// Setup-Endpunkt: Erstellt die developer:access Permission und fügt sie der Admin-Rolle hinzu -// Dieser Endpunkt erfordert keine Authentifizierung, da er nur einmalig zum Setup verwendet wird -router.post('/setup', async (req, res: Response) => { - try { - // Create or get the developer:access permission - const developerPerm = await prisma.permission.upsert({ - where: { resource_action: { resource: 'developer', action: 'access' } }, - update: {}, - create: { resource: 'developer', action: 'access' }, - }); - - // Get the Admin role - const adminRole = await prisma.role.findUnique({ - where: { name: 'Admin' }, - include: { permissions: true }, - }); - - if (!adminRole) { - res.status(404).json({ success: false, error: 'Admin-Rolle nicht gefunden' }); - return; - } - - // Check if Admin already has this permission - const hasPermission = adminRole.permissions.some( - (rp) => rp.permissionId === developerPerm.id - ); - - if (!hasPermission) { - await prisma.rolePermission.create({ - data: { - roleId: adminRole.id, - permissionId: developerPerm.id, - }, - }); - res.json({ success: true, message: 'developer:access Permission wurde zur Admin-Rolle hinzugefügt. Bitte neu einloggen!' }); - } else { - res.json({ success: true, message: 'Admin-Rolle hat bereits die developer:access Permission' }); - } - } catch (error) { - console.error('Setup error:', error); - res.status(500).json({ success: false, error: 'Fehler beim Setup' }); - } -}); +// HINWEIS: Der frühere `POST /setup`-Endpoint wurde entfernt (Pentest Runde 3 +// 2026-05-16 – KRITISCH). Er war ohne Auth erreichbar und konnte +// `developer:access` an die Admin-Rolle hängen → Privilege-Escalation auf +// volle DB-Kontrolle. Wenn die developer:access-Permission manuell gesetzt +// werden muss, gibt es das CLI-Script `prisma/add-developer-permission.ts`. // Tabellen-Metadaten mit Beziehungen const tableMetadata: Record