b3a6620da6
Pentest-Befund (MEDIUM): companyName und weitere Plain-Text-Setting- Keys nahmen via PUT /api/settings/:key XSS-Payloads wie <img src=x onerror=alert(1)> ungefiltert entgegen. Nur Admin triggerbar, aber E-Mail-Templates/PDF-Generatoren hätten den Wert unescaped rendern können. Fix in appSetting.service.ts: sanitizeSettingValue(key, value) strippt HTML außer für die expliziten Editor-Keys (imprintHtml, privacyPolicyHtml, authorizationTemplateHtml, websitePrivacyPolicyHtml). Greift in updateSetting + updateSettings. cleanup-xss-and-mass-assignment.ts bereinigt bestehende dreckige Werte beim Container-Start (idempotent). Live-verifiziert auf dev: - PUT companyName="<img onerror=alert(1)>OpenCRM<script>alert(2)</script>" → DB: "OpenCRM" - Bulk-PUT mit XSS auf companyName + defaultEmailDomain → gestrippt - imprintHtml mit "<h1>...<p>" → unverändert (HTML-allowed) - Cleanup-Skript auf dirty value: "EvilCo" statt mit Tags Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
134 lines
4.6 KiB
TypeScript
134 lines
4.6 KiB
TypeScript
import { Response } from 'express';
|
|
import prisma from '../lib/prisma.js';
|
|
import * as appSettingService from '../services/appSetting.service.js';
|
|
import { logChange } from '../services/audit.service.js';
|
|
import { ApiResponse, AuthRequest } from '../types/index.js';
|
|
|
|
export async function getAllSettings(req: AuthRequest, res: Response): Promise<void> {
|
|
try {
|
|
const settings = await appSettingService.getAllSettings();
|
|
res.json({ success: true, data: settings } as ApiResponse);
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Fehler beim Laden der Einstellungen',
|
|
} as ApiResponse);
|
|
}
|
|
}
|
|
|
|
export async function getPublicSettings(req: AuthRequest, res: Response): Promise<void> {
|
|
try {
|
|
const settings = await appSettingService.getPublicSettings();
|
|
res.json({ success: true, data: settings } as ApiResponse);
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Fehler beim Laden der Einstellungen',
|
|
} as ApiResponse);
|
|
}
|
|
}
|
|
|
|
export async function updateSetting(req: AuthRequest, res: Response): Promise<void> {
|
|
try {
|
|
const { key } = req.params;
|
|
const { value } = req.body;
|
|
|
|
if (value === undefined) {
|
|
res.status(400).json({
|
|
success: false,
|
|
error: 'Wert ist erforderlich',
|
|
} as ApiResponse);
|
|
return;
|
|
}
|
|
|
|
// Whitelist-Check (Pentest Runde 11, M1)
|
|
if (!appSettingService.isAllowedSettingKey(key)) {
|
|
res.status(400).json({
|
|
success: false,
|
|
error: `Unbekannter Setting-Key: ${key}`,
|
|
} as ApiResponse);
|
|
return;
|
|
}
|
|
|
|
// Vorherigen Stand laden für Audit
|
|
const before = await prisma.appSetting.findUnique({ where: { key } });
|
|
const oldValue = before?.value ?? '-';
|
|
// HTML-Tags aus Plain-Text-Keys strippen, bevor sie in der DB landen.
|
|
// Pentest 2026-05-19, MEDIUM: companyName="<img onerror=...>" landete
|
|
// sonst ungefiltert in E-Mail-Templates / PDFs.
|
|
const newValue = appSettingService.sanitizeSettingValue(key, String(value));
|
|
|
|
await appSettingService.setSetting(key, newValue);
|
|
|
|
const label = oldValue !== newValue
|
|
? `Einstellung "${key}" geändert: ${oldValue} → ${newValue}`
|
|
: `Einstellung "${key}" geändert`;
|
|
await logChange({
|
|
req, action: 'UPDATE', resourceType: 'AppSetting',
|
|
resourceId: key,
|
|
label,
|
|
details: oldValue !== newValue ? { [key]: { von: oldValue, nach: newValue } } : undefined,
|
|
});
|
|
res.json({ success: true, message: 'Einstellung gespeichert' } as ApiResponse);
|
|
} catch (error) {
|
|
res.status(400).json({
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Fehler beim Speichern der Einstellung',
|
|
} as ApiResponse);
|
|
}
|
|
}
|
|
|
|
export async function updateSettings(req: AuthRequest, res: Response): Promise<void> {
|
|
try {
|
|
const settings = req.body;
|
|
|
|
if (!settings || typeof settings !== 'object') {
|
|
res.status(400).json({
|
|
success: false,
|
|
error: 'Einstellungen sind erforderlich',
|
|
} as ApiResponse);
|
|
return;
|
|
}
|
|
|
|
// Whitelist-Check für jeden Key (Pentest Runde 11, M1: Mass Assignment)
|
|
const unknownKeys = Object.keys(settings).filter(
|
|
(k) => !appSettingService.isAllowedSettingKey(k),
|
|
);
|
|
if (unknownKeys.length > 0) {
|
|
res.status(400).json({
|
|
success: false,
|
|
error: `Unbekannte Setting-Keys: ${unknownKeys.join(', ')}`,
|
|
} as ApiResponse);
|
|
return;
|
|
}
|
|
|
|
// Vorherige Werte laden für Audit
|
|
const changes: Record<string, { von: unknown; nach: unknown }> = {};
|
|
for (const [key, value] of Object.entries(settings)) {
|
|
const before = await prisma.appSetting.findUnique({ where: { key } });
|
|
const oldValue = before?.value ?? '-';
|
|
const newValue = appSettingService.sanitizeSettingValue(key, String(value));
|
|
if (oldValue !== newValue) {
|
|
changes[key] = { von: oldValue, nach: newValue };
|
|
}
|
|
await appSettingService.setSetting(key, newValue);
|
|
}
|
|
|
|
const changeList = Object.entries(changes).map(([k, c]) => `${k}: ${c.von} → ${c.nach}`).join(', ');
|
|
await logChange({
|
|
req, action: 'UPDATE', resourceType: 'AppSetting',
|
|
label: changeList
|
|
? `Einstellungen aktualisiert: ${changeList}`
|
|
: `Einstellungen aktualisiert (${Object.keys(settings).join(', ')})`,
|
|
details: Object.keys(changes).length > 0 ? changes : undefined,
|
|
});
|
|
|
|
res.json({ success: true, message: 'Einstellungen gespeichert' } as ApiResponse);
|
|
} catch (error) {
|
|
res.status(400).json({
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Fehler beim Speichern der Einstellungen',
|
|
} as ApiResponse);
|
|
}
|
|
}
|