Security fixes N4/H2/H1 + hide template editor for non-admins
This commit is contained in:
49
.env.example
49
.env.example
@@ -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
|
|
||||||
@@ -77,8 +77,18 @@ async function browseOUTree(searchBase) {
|
|||||||
throw new Error('LDAP nicht konfiguriert.');
|
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;
|
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 {
|
try {
|
||||||
const { searchEntries } = await client.search(base, {
|
const { searchEntries } = await client.search(base, {
|
||||||
@@ -342,10 +352,19 @@ async function searchADGroups(query) {
|
|||||||
throw new Error('LDAP nicht konfiguriert.');
|
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();
|
const client = await createClient();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const escapedQuery = query.replace(/[()*\\]/g, '\\$&');
|
const escapedQuery = safeQuery.replace(/[()*\\]/g, '\\$&');
|
||||||
const { searchEntries } = await client.search(LDAP_SEARCH_BASE, {
|
const { searchEntries } = await client.search(LDAP_SEARCH_BASE, {
|
||||||
filter: `(&(objectClass=group)(|(cn=*${escapedQuery}*)(displayName=*${escapedQuery}*)(sAMAccountName=*${escapedQuery}*)))`,
|
filter: `(&(objectClass=group)(|(cn=*${escapedQuery}*)(displayName=*${escapedQuery}*)(sAMAccountName=*${escapedQuery}*)))`,
|
||||||
scope: 'sub',
|
scope: 'sub',
|
||||||
|
|||||||
@@ -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'];
|
: ['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 }));
|
app.use(cors({ origin: allowedOrigins, credentials: true }));
|
||||||
// Punkt 14: Body-Size-Limit to prevent DoS via large payloads
|
// 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
|
// Punkt 8: Cookie-Parser für HttpOnly-Cookie Auth
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
backups/workflow_20260825_115300.sql.gz
Normal file
BIN
backups/workflow_20260825_115300.sql.gz
Normal file
Binary file not shown.
BIN
backups/workflow_20260825_175259.sql.gz
Normal file
BIN
backups/workflow_20260825_175259.sql.gz
Normal file
Binary file not shown.
BIN
backups/workflow_20260825_235256.sql.gz
Normal file
BIN
backups/workflow_20260825_235256.sql.gz
Normal file
Binary file not shown.
BIN
backups/workflow_20260826_092234.sql.gz
Normal file
BIN
backups/workflow_20260826_092234.sql.gz
Normal file
Binary file not shown.
BIN
backups/workflow_20260826_152233.sql.gz
Normal file
BIN
backups/workflow_20260826_152233.sql.gz
Normal file
Binary file not shown.
BIN
backups/workflow_20260826_212231.sql.gz
Normal file
BIN
backups/workflow_20260826_212231.sql.gz
Normal file
Binary file not shown.
BIN
backups/workflow_20260827_082653.sql.gz
Normal file
BIN
backups/workflow_20260827_082653.sql.gz
Normal file
Binary file not shown.
BIN
backups/workflow_20260827_142652.sql.gz
Normal file
BIN
backups/workflow_20260827_142652.sql.gz
Normal file
Binary file not shown.
BIN
backups/workflow_20260827_202650.sql.gz
Normal file
BIN
backups/workflow_20260827_202650.sql.gz
Normal file
Binary file not shown.
BIN
backups/workflow_20260828_022649.sql.gz
Normal file
BIN
backups/workflow_20260828_022649.sql.gz
Normal file
Binary file not shown.
@@ -1,3 +0,0 @@
|
|||||||
# Frontend Environment Variables
|
|
||||||
# API base URL for backend (default: http://localhost:5000/api)
|
|
||||||
VITE_API_BASE=http://localhost:5000/api
|
|
||||||
@@ -50,8 +50,14 @@ function AppContent() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Punkt 1: Immediate tab switch - no skeleton/transition delay to avoid bounce
|
// 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) => {
|
const handleTabChange = (newTab) => {
|
||||||
if (newTab === activeTab) return;
|
if (newTab === activeTab) return;
|
||||||
|
if (user?.role !== 'admin' && ADMIN_TABS.includes(newTab)) {
|
||||||
|
setActiveTab('dashboard');
|
||||||
|
return;
|
||||||
|
}
|
||||||
setActiveTab(newTab);
|
setActiveTab(newTab);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ export default function Sidebar({ activeTab, onTabChange }) {
|
|||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ id: 'dashboard', label: 'Vorlagen' },
|
{ 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: 'tasks', label: 'Aufgaben' }] : []),
|
||||||
...(user?.role === 'admin' ? [{ id: 'users', label: 'Nutzerverwaltung' }] : []),
|
...(user?.role === 'admin' ? [{ id: 'users', label: 'Nutzerverwaltung' }] : []),
|
||||||
...(user?.role === 'admin' ? [{ id: 'auditlog', label: 'Audit-Log' }] : []),
|
...(user?.role === 'admin' ? [{ id: 'auditlog', label: 'Audit-Log' }] : []),
|
||||||
|
|||||||
Reference in New Issue
Block a user