Security & UX Release v7
Security: - H1: Stored-XSS-Fix — Upload-Pfad-Whitelist (Server + Frontend-Guard safeFileUrl) - H2: Transaktionen repariert — txDb-Contract in db.js (PG + SQLite), Rollback funktioniert - H3-Vorbereitung: SESSION_SECRET wird in Compose durchgereicht (Fix M3) - registerLimiter exportiert (Crash-Bug: Route.post ohne Callback) - LDAP-Sync: PG-Transaktionsabbruch bei UNIQUE-Verstoß behoben (Precheck-Selects) - LDAP-Filter: nur echte Benutzerkonten (keine Computer/Service-Accounts, Bit 512) - Rollen app-seitig: Sync ändert nie role/status, neue User immer user+inaktiv - DB-Cleanup: 82 Computer-/Service-Accounts aus lokaler User-Tabelle entfernt UX: - Dashboard: Vorlagen als Table-Liste + Column-Chart (Top 5 in %), 2 gleich große Spalten - Table-Listen (Dashboard/Vorlageneditor/Aufgaben) scrollbar bis Seitenende - Pagination 10/Seite im Dashboard, Sidebar-Label Dashboard
This commit is contained in:
@@ -11,20 +11,20 @@ const db = require('../db');
|
||||
const { auditLog } = require('../auditLog');
|
||||
const { authMiddleware, adminMiddleware, createSession, deleteSession, invalidateUserSessions, isAccountLocked, recordFailedLogin, recordSuccessfulLogin, setAuthCookie, clearAuthCookie, setCSRFCookie, hashToken } = require('../middleware/auth');
|
||||
const { isLDAPConfigured, authenticateLDAP } = require('../ldapSync');
|
||||
const { loginLimiter } = require('../middleware/rateLimit');
|
||||
const { loginLimiter, registerLimiter } = require('../middleware/rateLimit');
|
||||
const { validate, registerSchema, loginSchema } = require('../middleware/validation');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Register
|
||||
router.post('/register', validate(registerSchema), async (req, res) => {
|
||||
router.post('/register', registerLimiter, validate(registerSchema), async (req, res) => {
|
||||
const { email, password, name } = req.validatedBody;
|
||||
try {
|
||||
const hash = bcrypt.hashSync(password, 10);
|
||||
// VULN-FIX: Force role to 'user' - never trust client-supplied role on register
|
||||
const info = await db.prepare('INSERT INTO users (email, password, name, role, status, source) VALUES (?, ?, ?, \'user\', \'inaktiv\', \'local\')').run(email, hash, name);
|
||||
const userId = info.lastInsertRowid;
|
||||
auditLog(null, 'register', 'user', userId, `New registration: ${email}`);
|
||||
auditLog(null, 'register', 'user', userId, `New registration: ${email}`, req);
|
||||
// P4: Set CSRF cookie for the new session
|
||||
const csrfToken = setCSRFCookie(res);
|
||||
// Return correct status 'inaktiv' (Punkt 5 fix)
|
||||
@@ -54,8 +54,9 @@ router.post('/login', loginLimiter, validate(loginSchema), async (req, res) => {
|
||||
try {
|
||||
const ldapResult = await authenticateLDAP(email, password);
|
||||
const adRow = await db.prepare('SELECT id, email, name, role, status, source, username FROM users WHERE LOWER(username) = LOWER(?)').get(ldapResult.username);
|
||||
if (!adRow) return res.status(404).json({ error: 'Nutzer im System nicht gefunden. Bitte warte auf die naechste Synchronisation.' });
|
||||
if (adRow.status === 'inaktiv') return res.status(403).json({ error: 'Dein Konto ist deaktiviert.' });
|
||||
// M3: Unified error messages — don't reveal whether the account exists
|
||||
if (!adRow) return res.status(401).json({ error: 'Ungueltige Anmeldedaten.' });
|
||||
if (adRow.status === 'inaktiv') return res.status(401).json({ error: 'Ungueltige Anmeldedaten.' });
|
||||
await recordSuccessfulLogin(adRow.id);
|
||||
// V6: Pass old token for session rotation (prevents session fixation)
|
||||
const oldToken = req.cookies?.workflow_token || req.headers.authorization?.replace('Bearer ', '');
|
||||
@@ -63,7 +64,7 @@ router.post('/login', loginLimiter, validate(loginSchema), async (req, res) => {
|
||||
setAuthCookie(res, rawToken); // Punkt 8: HttpOnly-Cookie
|
||||
// P4: CSRF cookie derived from the new session token (deterministic, self-healing)
|
||||
const csrfToken = setCSRFCookie(res, hashToken(rawToken));
|
||||
auditLog(adRow.id, 'login', 'user', adRow.id, 'AD login');
|
||||
auditLog(adRow.id, 'login', 'user', adRow.id, 'AD login', req);
|
||||
// Bug 6: Don't expose token in response body (cookie-only auth)
|
||||
res.json({ ...adRow, csrfToken });
|
||||
} catch (ldapErr) {
|
||||
@@ -76,12 +77,14 @@ router.post('/login', loginLimiter, validate(loginSchema), async (req, res) => {
|
||||
}
|
||||
|
||||
if (row.status === 'inaktiv') {
|
||||
return res.status(403).json({ error: 'Dein Konto ist deaktiviert. Bitte wende dich an einen Administrator.' });
|
||||
// M3: Unified error message — don't reveal whether the account exists
|
||||
return res.status(401).json({ error: 'Ungueltige Anmeldedaten.' });
|
||||
}
|
||||
|
||||
if (row.source === 'ad') {
|
||||
if (!isLDAPConfigured()) {
|
||||
return res.status(403).json({ error: 'AD-Anmeldung nicht konfiguriert.' });
|
||||
// M3: Unified error message
|
||||
return res.status(401).json({ error: 'Ungueltige Anmeldedaten.' });
|
||||
}
|
||||
try {
|
||||
await authenticateLDAP(row.username || row.email.split('@')[0], password);
|
||||
@@ -92,7 +95,7 @@ router.post('/login', loginLimiter, validate(loginSchema), async (req, res) => {
|
||||
setAuthCookie(res, rawToken); // Punkt 8: HttpOnly-Cookie
|
||||
// P4: CSRF cookie derived from the new session token (deterministic, self-healing)
|
||||
const csrfToken = setCSRFCookie(res, hashToken(rawToken));
|
||||
auditLog(row.id, 'login', 'user', row.id, 'AD login');
|
||||
auditLog(row.id, 'login', 'user', row.id, 'AD login', req);
|
||||
const { password: _, ...safeRow } = row;
|
||||
// Bug 6: Don't expose token in response body (cookie-only auth)
|
||||
res.json({ ...safeRow, csrfToken });
|
||||
@@ -110,7 +113,7 @@ router.post('/login', loginLimiter, validate(loginSchema), async (req, res) => {
|
||||
passwordMatch = row.password === password;
|
||||
if (passwordMatch) {
|
||||
// P8: Log plaintext login for security monitoring (auto-upgrade follows)
|
||||
auditLog(row.id, 'plaintext_login_upgraded', 'user', row.id, 'Legacy plaintext password upgraded to bcrypt');
|
||||
auditLog(row.id, 'plaintext_login_upgraded', 'user', row.id, 'Legacy plaintext password upgraded to bcrypt', req);
|
||||
console.warn('[SECURITY] User', row.email, 'logged in with plaintext password - upgrading to bcrypt.');
|
||||
const hash = bcrypt.hashSync(password, 12); // Punkt 10: 12 rounds per OWASP
|
||||
await db.prepare('UPDATE users SET password = ? WHERE id = ?').run(hash, row.id);
|
||||
@@ -127,7 +130,7 @@ router.post('/login', loginLimiter, validate(loginSchema), async (req, res) => {
|
||||
setAuthCookie(res, rawToken); // Punkt 8: HttpOnly-Cookie
|
||||
// P4: CSRF cookie derived from the new session token (deterministic, self-healing)
|
||||
const csrfToken = setCSRFCookie(res, hashToken(rawToken));
|
||||
auditLog(row.id, 'login', 'user', row.id, 'Local login');
|
||||
auditLog(row.id, 'login', 'user', row.id, 'Local login', req);
|
||||
const { password: _, ...safeRow } = row;
|
||||
// Bug 6: Don't expose token in response body (cookie-only auth)
|
||||
res.json({ ...safeRow, csrfToken });
|
||||
@@ -137,7 +140,7 @@ router.post('/login', loginLimiter, validate(loginSchema), async (req, res) => {
|
||||
// Logout
|
||||
router.post('/logout', async (req, res) => {
|
||||
const rawToken = req.cookies?.workflow_token || req.headers.authorization?.replace('Bearer ', '');
|
||||
await deleteSession(rawToken);
|
||||
await deleteSession(rawToken, req);
|
||||
clearAuthCookie(res); // Punkt 8: Clear HttpOnly-Cookie
|
||||
res.json({ message: 'Abgemeldet.' });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user