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:
Kühn
2026-09-10 16:57:20 +02:00
parent ec2ed91621
commit 1aec65dc95
86 changed files with 636 additions and 208 deletions

View File

@@ -9,7 +9,17 @@ const db = require('../db');
const { auditLog } = require('../auditLog');
// P6: Cookie config - defined early for use in CSRF and auth cookies
// M1: Cookie security is now configurable via COOKIE_SECURE env var so that
// plain-HTTP deployments (e.g. behind a TLS-terminating proxy that sets
// X-Forwarded-Proto) can still use secure cookies, while HTTP-only dev/test
// setups can disable them. Defaults to NODE_ENV === 'production'.
// COOKIE_SECURE=true → always secure
// COOKIE_SECURE=false → never secure (HTTP dev)
// unset → secure in production, lax in development
const isProduction = process.env.NODE_ENV === 'production';
const COOKIE_SECURE = process.env.COOKIE_SECURE !== undefined
? process.env.COOKIE_SECURE === 'true'
: isProduction;
const COOKIE_NAME = 'workflow_token';
function hashToken(token) {
@@ -24,7 +34,18 @@ const CSRF_HEADER_NAME = 'x-csrf-token';
// Advantage: cookie and header can never drift apart (no more stale-token 403s),
// works across tabs, page reloads and re-logins. The cookie is self-healed by
// authMiddleware on every request if it is missing or out of sync.
const CSRF_SECRET = process.env.SESSION_SECRET || 'workflow-portal-csrf-v1';
// M2: Fail-fast in production if SESSION_SECRET is missing — a hardcoded
// fallback secret in the source tree is a security risk.
const SESSION_SECRET = process.env.SESSION_SECRET || '';
if (!SESSION_SECRET) {
if (process.env.NODE_ENV === 'production') {
console.error('[FATAL] SESSION_SECRET Umgebungsvariable ist in der Produktion nicht gesetzt. Setze sie auf einen langen, zufälligen Wert.');
process.exit(1);
} else {
console.warn('[WARN] SESSION_SECRET nicht gesetzt — verwende unsicheren Fallback nur für die Entwicklung.');
}
}
const CSRF_SECRET = SESSION_SECRET || 'workflow-portal-csrf-dev-only-fallback';
function deriveCSRFToken(tokenHash) {
return crypto.createHmac('sha256', CSRF_SECRET).update(tokenHash).digest('hex');
@@ -36,7 +57,7 @@ function setCSRFCookie(res, tokenHash) {
const csrfToken = tokenHash ? deriveCSRFToken(tokenHash) : crypto.randomBytes(32).toString('hex');
res.cookie(CSRF_COOKIE_NAME, csrfToken, {
httpOnly: false, // Must be readable by JS to send back in header
secure: isProduction,
secure: COOKIE_SECURE,
sameSite: isProduction ? 'strict' : 'lax',
maxAge: 24 * 60 * 60 * 1000, // 24h (re-set by authMiddleware on every request)
path: '/',
@@ -65,7 +86,7 @@ function csrfMiddleware(req, res, next) {
if (req.tokenHash && cookieToken !== deriveCSRFToken(req.tokenHash)) {
res.cookie(CSRF_COOKIE_NAME, deriveCSRFToken(req.tokenHash), {
httpOnly: false,
secure: isProduction,
secure: COOKIE_SECURE,
sameSite: isProduction ? 'strict' : 'lax',
maxAge: 24 * 60 * 60 * 1000,
path: '/',
@@ -99,7 +120,7 @@ async function authMiddleware(req, res, next) {
if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
res.cookie(CSRF_COOKIE_NAME, deriveCSRFToken(tokenHash), {
httpOnly: false,
secure: isProduction,
secure: COOKIE_SECURE,
sameSite: isProduction ? 'strict' : 'lax',
maxAge: 24 * 60 * 60 * 1000,
path: '/',
@@ -141,12 +162,12 @@ async function createSession(userId, oldRawToken) {
return rawToken;
}
async function deleteSession(rawToken) {
async function deleteSession(rawToken, req) {
if (!rawToken) return;
const tokenHash = hashToken(rawToken);
const session = await db.prepare('SELECT user_id FROM sessions WHERE token = ?').get(tokenHash);
if (session) {
auditLog(session.user_id, 'logout', 'user', session.user_id, null);
auditLog(session.user_id, 'logout', 'user', session.user_id, null, req);
}
await db.prepare('DELETE FROM sessions WHERE token = ?').run(tokenHash);
}
@@ -190,7 +211,7 @@ function setAuthCookie(res, token) {
const ttlHours = parseInt(process.env.SESSION_TTL_HOURS) || 168;
res.cookie(COOKIE_NAME, token, {
httpOnly: true,
secure: isProduction,
secure: COOKIE_SECURE,
sameSite: isProduction ? 'strict' : 'lax',
maxAge: ttlHours * 60 * 60 * 1000,
path: '/',
@@ -204,6 +225,6 @@ function clearAuthCookie(res) {
module.exports = {
authMiddleware, adminMiddleware, createSession, deleteSession, invalidateUserSessions, hashToken,
isAccountLocked, recordFailedLogin, recordSuccessfulLogin,
setAuthCookie, clearAuthCookie, COOKIE_NAME,
setAuthCookie, clearAuthCookie, COOKIE_NAME, COOKIE_SECURE,
setCSRFCookie, csrfMiddleware, CSRF_COOKIE_NAME, CSRF_HEADER_NAME
};