Files
workflow-app/backend/middleware/validation.js
Kühn 6be1791c62 DEV1.0: Initial commit - Workflow Portal with security fixes
- Backend: Express.js + PostgreSQL/SQLite with LDAP/AD integration
- Frontend: React 18 + Vite + TailwindCSS/DaisyUI
- Security fixes applied (2026-07 + 2026-08):
  - LDAP injection prevention, CSRF protection, HttpOnly cookies
  - Session hashing (SHA-256), account lockout, rate limiting
  - Input validation (zod), file upload security, CSP/HSTS headers
  - V3: express-rate-limit updated (ip-address SSRF fix)
  - V4: postcss updated (nanoid DoS fix)
  - V5: Rate-limit on /health endpoint
  - V6: Session rotation on login (session fixation prevention)
  - V9: Task values array limit (DoS prevention)
  - V10: Frontend XSS audit completed
- Docker: Multi-stage build, non-root user, PostgreSQL + backup service
2026-08-24 09:45:28 +02:00

209 lines
7.4 KiB
JavaScript

/**
* Input Validation Module (zod)
*
* Punkt 2: Schema-based input validation for all API routes.
* Provides reusable validation schemas and a middleware helper.
*/
const { z } = require('zod');
// ============ Auth Schemas ============
// Punkt 11: Password-Policy - min 8 chars, uppercase, lowercase, number
const passwordSchema = z.string()
.min(8, 'Passwort muss mindestens 8 Zeichen lang sein.')
.regex(/[A-Z]/, 'Passwort muss mindestens einen Grossbuchstaben enthalten.')
.regex(/[a-z]/, 'Passwort muss mindestens einen Kleinbuchstaben enthalten.')
.regex(/[0-9]/, 'Passwort muss mindestens eine Zahl enthalten.');
const registerSchema = z.object({
email: z.string().email('Ungueltige E-Mail-Adresse.'),
password: passwordSchema,
name: z.string().max(100).optional().default(''),
// VULN-FIX: role removed - always 'user' on register, never trust client
});
const loginSchema = z.object({
email: z.string().min(1, 'E-Mail ist erforderlich.'),
password: z.string().min(1, 'Passwort ist erforderlich.'),
});
// ============ User Schemas ============
const createUserSchema = z.object({
email: z.string().email('Ungueltige E-Mail-Adresse.'),
password: passwordSchema,
name: z.string().max(100).optional().default(''),
role: z.enum(['admin', 'user']).optional().default('user'),
status: z.enum(['aktiv', 'inaktiv']).optional().default('aktiv'),
});
const updateUserSchema = z.object({
email: z.string().email('Ungueltige E-Mail-Adresse.').optional(),
name: z.string().max(100).optional(),
password: passwordSchema.optional(),
current_password: z.string().optional(),
role: z.enum(['admin', 'user']).optional(),
status: z.enum(['aktiv', 'inaktiv']).optional(),
});
// ============ Template Schemas ============
const templateStepSchema = z.object({
page_num: z.number().int().min(1).optional().default(1),
label: z.string().min(1, 'Label ist erforderlich.').max(200),
type: z.enum(['checkbox', 'text_input', 'file_upload', 'email', 'dropdown', 'ad_password', 'ad_displayname']),
step_order: z.number().int().min(0).optional(),
email_domain: z.string().optional(),
email_source_fields: z.string().optional(),
dropdown_options: z.string().optional(),
ad_field: z.string().optional(),
ad_prefix: z.string().optional(),
hidden: z.boolean().optional().default(false),
});
const createTemplateSchema = z.object({
name: z.string().min(1, 'Name ist erforderlich.').max(200),
description: z.string().max(1000).optional().default(''),
is_assignable: z.boolean().optional().default(false),
allows_file_upload: z.boolean().optional().default(false),
ad_create: z.boolean().optional().default(false),
steps: z.array(templateStepSchema).optional().default([]),
});
const updateTemplateSchema = z.object({
name: z.string().min(1, 'Name ist erforderlich.').max(200),
description: z.string().max(1000).optional().default(''),
is_assignable: z.boolean().optional().default(false),
allows_file_upload: z.boolean().optional().default(false),
ad_create: z.boolean().optional().default(false),
steps: z.array(templateStepSchema).optional().default([]),
});
// ============ Task Schemas ============
const createTaskSchema = z.object({
template_id: z.number().int().positive('Template-ID ist erforderlich.'),
title: z.string().min(1, 'Titel ist erforderlich.').max(500),
user_id: z.number().int().positive().optional(),
file_path: z.string().optional(),
// V9: Limit task values array to prevent DoS via huge payloads
values: z.array(z.object({
step_id: z.number().int().positive().optional(),
value: z.string().max(10000).optional(),
is_checked: z.boolean().optional(),
file_path: z.string().optional(),
})).max(100, 'Maximal 100 Werte pro Aufgabe erlaubt.').optional().default([]),
});
const updateTaskStatusSchema = z.object({
status: z.enum(['offen', 'erledigt'], { message: 'Status muss "offen" oder "erledigt" sein.' }),
});
const updateTaskValuesSchema = z.object({
values: z.array(z.object({
id: z.number().int().positive(),
value: z.string().optional(),
is_checked: z.boolean().optional(),
})).min(1, 'Mindestens ein Wert ist erforderlich.'),
});
const addTaskFieldSchema = z.object({
label: z.string().min(1, 'Label ist erforderlich.').max(200),
type: z.enum(['text_input', 'checkbox', 'dropdown', 'email']).optional().default('text_input'),
value: z.string().optional().default(''),
page_num: z.number().int().min(1).optional().default(1),
dropdown_options: z.string().optional().default(''),
ad_field: z.string().optional().default(''),
hidden: z.boolean().optional().default(false),
email_source_fields: z.string().optional().default(''),
});
// ============ AD Schemas ============
const createADUserSchema = z.object({
ou: z.string().min(1, 'OU ist erforderlich.'),
vorname: z.string().min(1, 'Vorname ist erforderlich.').max(100),
nachname: z.string().min(1, 'Nachname ist erforderlich.').max(100),
username: z.string().min(1, 'Anmeldename ist erforderlich.').max(50),
password: z.string().min(1, 'Passwort ist erforderlich.').min(8, 'Passwort muss mindestens 8 Zeichen lang sein.'),
email: z.string().email().optional(),
department: z.string().max(100).optional(),
telefon: z.string().max(50).optional(),
titel: z.string().max(100).optional(),
displayName: z.string().max(200).optional(),
physicalDeliveryOfficeName: z.string().max(100).optional(),
company: z.string().max(100).optional(),
description: z.string().max(500).optional(),
wWWHomePage: z.string().max(200).optional(),
streetAddress: z.string().max(200).optional(),
postOfficeBox: z.string().max(50).optional(),
l: z.string().max(100).optional(),
st: z.string().max(100).optional(),
postalCode: z.string().max(20).optional(),
c: z.string().max(2).optional(),
groups: z.array(z.string()).optional(),
});
const deleteADUserSchema = z.object({
dn: z.string().min(1, 'DN ist erforderlich.'),
});
// ============ Search/Query Schemas ============
const paginationSchema = z.object({
page: z.coerce.number().int().min(1).optional().default(1),
limit: z.coerce.number().int().min(1).max(100).optional().default(20),
});
const searchSchema = z.object({
search: z.string().max(100).optional(),
});
// ============ Validation Middleware ============
function validate(schema) {
return (req, res, next) => {
try {
const result = schema.safeParse(req.body);
if (!result.success) {
const errors = result.error.errors.map(e => e.message).join(', ');
return res.status(400).json({ error: errors });
}
req.validatedBody = result.data;
next();
} catch (err) {
return res.status(400).json({ error: 'Ungueltige Eingabe.' });
}
};
}
function validateQuery(schema) {
return (req, res, next) => {
try {
const result = schema.safeParse(req.query);
if (!result.success) {
const errors = result.error.errors.map(e => e.message).join(', ');
return res.status(400).json({ error: errors });
}
req.validatedQuery = result.data;
next();
} catch (err) {
return res.status(400).json({ error: 'Ungueltige Abfrage.' });
}
};
}
module.exports = {
// Schemas
registerSchema,
loginSchema,
createUserSchema,
updateUserSchema,
createTemplateSchema,
updateTemplateSchema,
templateStepSchema,
createTaskSchema,
updateTaskStatusSchema,
updateTaskValuesSchema,
addTaskFieldSchema,
createADUserSchema,
deleteADUserSchema,
paginationSchema,
searchSchema,
// Middleware
validate,
validateQuery,
};