Security fixes N4/H2/H1 + hide template editor for non-admins

This commit is contained in:
Kühn
2026-08-28 10:15:31 +02:00
parent a35721abec
commit 03d20b0e09
19 changed files with 31 additions and 56 deletions

View File

@@ -1,49 +0,0 @@
# ============================================================
# Workflow Portal - Environment Configuration
# ============================================================
# Kopiere diese Datei zu .env und passe die Werte an.
# Alle Werte in <> müssen ausgefüllt werden.
# Werte mit Defaults können auskommentiert oder belassen werden.
# ============================================================
# ============ LDAP / Active Directory ============
LDAP_SERVER=PIDC02.seatle.intra
LDAP_PORT=636
LDAP_SEARCH_BASE=<z.B. DC=SEATLE,DC=INTRA>
LDAP_DOMAIN=SEATLE
LDAP_IGNORE_CERT_ERRORS=true
LDAP_BIND_USER=<z.B. svc_workflow@seatle.intra>
LDAP_BIND_PASSWORD=<LDAP-Service-Account-Passwort>
LDAP_SYNC_INTERVAL=300000
LDAP_FILTER=
LDAP_ATTRIBUTES=mail,displayName,memberOf,distinguishedName,sAMAccountName
LDAP_CREATE_OU=<z.B. OU=Users,OU=SEATLE,DC=SEATLE,DC=INTRA>
LDAP_UPN_SUFFIX=<z.B. seatle.intra>
# ============ Admin Account ============
ADMIN_EMAIL=admin@workflow.local
ADMIN_INIT_PASSWORD=<Admin-Initial-Passwort, min. 8 Zeichen mit Groß-/Kleinbuchstaben + Zahl>
# ============ Server ============
PORT=5000
NODE_ENV=production
CORS_ORIGIN=http://localhost:3900
# ============ PostgreSQL Database ============
POSTGRES_DB=workflow
POSTGRES_USER=workflow
POSTGRES_PASSWORD=<Sicheres Datenbank-Passwort>
# DATABASE_URL wird automatisch aus den Werten oben generiert:
# postgresql://workflow:<POSTGRES_PASSWORD>@db:5432/workflow
# ============ Security ============
SESSION_MAX_PER_USER=5
SESSION_TTL_HOURS=168
LOGIN_MAX_ATTEMPTS=5
LOGIN_LOCKOUT_MINUTES=15
BODY_LIMIT=1mb
UPLOAD_MAX_MB=10
# ============ DB Backup ============
BACKUP_INTERVAL_HOURS=6
BACKUP_RETENTION_DAYS=30

View File

@@ -77,8 +77,18 @@ async function browseOUTree(searchBase) {
throw new Error('LDAP nicht konfiguriert.');
}
const client = await createClient();
// H1: Validate searchBase - must be a DN under the configured LDAP_SEARCH_BASE
// (prevents arbitrary LDAP tree browsing outside the allowed scope)
const base = searchBase || LDAP_SEARCH_BASE;
if (base !== LDAP_SEARCH_BASE) {
const escapeDNRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const basePattern = new RegExp(',' + escapeDNRegex(LDAP_SEARCH_BASE) + '$', 'i');
if (!basePattern.test(base)) {
throw new Error('Ungültige Suchbasis: muss unterhalb von ' + LDAP_SEARCH_BASE + ' liegen.');
}
}
const client = await createClient();
try {
const { searchEntries } = await client.search(base, {
@@ -342,10 +352,19 @@ async function searchADGroups(query) {
throw new Error('LDAP nicht konfiguriert.');
}
// H2: Validate query - length limit + only safe characters (prevents LDAP injection & DoS)
const safeQuery = String(query || '').trim();
if (!safeQuery || safeQuery.length < 2 || safeQuery.length > 100) {
return [];
}
if (!/^[a-zA-Z0-9äöüÄÖÜß._\- ]+$/.test(safeQuery)) {
return [];
}
const client = await createClient();
try {
const escapedQuery = query.replace(/[()*\\]/g, '\\$&');
const escapedQuery = safeQuery.replace(/[()*\\]/g, '\\$&');
const { searchEntries } = await client.search(LDAP_SEARCH_BASE, {
filter: `(&(objectClass=group)(|(cn=*${escapedQuery}*)(displayName=*${escapedQuery}*)(sAMAccountName=*${escapedQuery}*)))`,
scope: 'sub',

View File

@@ -87,7 +87,8 @@ const allowedOrigins = validCorsOrigins.length > 0
: ['http://localhost:5000', 'http://localhost:5173', 'http://127.0.0.1:5000', 'http://127.0.0.1:5173'];
app.use(cors({ origin: allowedOrigins, credentials: true }));
// Punkt 14: Body-Size-Limit to prevent DoS via large payloads
app.use(express.json({ limit: process.env.BODY_LIMIT || '1mb' }));
// N4: 10MB to accommodate file uploads (matches UPLOAD_MAX_MB)
app.use(express.json({ limit: process.env.BODY_LIMIT || '10mb' }));
// Punkt 8: Cookie-Parser für HttpOnly-Cookie Auth
app.use(cookieParser());

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,3 +0,0 @@
# Frontend Environment Variables
# API base URL for backend (default: http://localhost:5000/api)
VITE_API_BASE=http://localhost:5000/api

View File

@@ -50,8 +50,14 @@ function AppContent() {
};
// Punkt 1: Immediate tab switch - no skeleton/transition delay to avoid bounce
// Admin-only tabs: non-admins are redirected to dashboard (defense in depth)
const ADMIN_TABS = ['templates', 'tasks', 'users', 'auditlog'];
const handleTabChange = (newTab) => {
if (newTab === activeTab) return;
if (user?.role !== 'admin' && ADMIN_TABS.includes(newTab)) {
setActiveTab('dashboard');
return;
}
setActiveTab(newTab);
};

View File

@@ -27,7 +27,8 @@ export default function Sidebar({ activeTab, onTabChange }) {
const tabs = [
{ id: 'dashboard', label: 'Vorlagen' },
{ id: 'templates', label: 'Vorlageneditor' },
// Vorlageneditor nur für Admins sichtbar
...(user?.role === 'admin' ? [{ id: 'templates', label: 'Vorlageneditor' }] : []),
...(user?.role === 'admin' ? [{ id: 'tasks', label: 'Aufgaben' }] : []),
...(user?.role === 'admin' ? [{ id: 'users', label: 'Nutzerverwaltung' }] : []),
...(user?.role === 'admin' ? [{ id: 'auditlog', label: 'Audit-Log' }] : []),