Files
opencrm/backend/src/controllers/appSetting.controller.ts
T
duffyduck 100147107c Pentest 2026-05-28 LOW 34.5: Backend-Validierung für AppSettings
Schema-Whitelist und Trailing-Slash-Strip für portalLoginUrl standen
NUR im Frontend. Der API-Endpoint nahm sonst /relative/path,
javascript:/ftp:/data:-Schemata und private IPs ungeprüft entgegen –
das landet als toter / bösartiger Link in den an Kunden verschickten
Portal-Mails (Open-Redirect / SSRF-Vektor).

Neuer validateSettingValue(key, value) in appSetting.service mit
per-Key-Logik:
  - portalLoginUrl: absolute http(s)-URL, isBlockedSsrfHost-Check
    (Cloud-Metadata immer, private Ranges via SSRF_BLOCK_PRIVATE_IPS),
    Trailing-Slash-Strip.
  - Schwellenwerte (deadline*/documentExpiry*): positive Integer.
  - Bool-Settings: strict 'true'/'false'.
  - monitoringAlertEmail: RFC-5322-light gegen Header-Injection.
  - Andere Keys: kein Format-Check (Default).

Controller (updateSetting + updateSettings) rufen Validator nach
stripHtml; bei Fehler HTTP 400 mit klarer Message. Bulk-PUT
validiert ALLE Werte VOR dem ersten DB-Write – kein halb-committed
State bei einem ungültigen Eintrag.

Live-verifiziert auf dev: alle Test-Payloads aus dem Pentest
sauber abgelehnt, legitime Werte (https-URL, Trailing-Slash, Pfade)
korrekt akzeptiert + normalisiert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 14:39:45 +02:00

155 lines
5.7 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 stripped = appSettingService.sanitizeSettingValue(key, String(value));
// Schema-spezifische Validierung (URL/Email/Int/Bool). Pentest
// 2026-05-28, LOW 34.5: portalLoginUrl nahm `/relative/path` und
// `http://192.168.1.1` ungefiltert entgegen → Open-Redirect /
// SSRF in der versendeten Mail.
const validation = appSettingService.validateSettingValue(key, stripped);
if (!validation.ok) {
res.status(400).json({ success: false, error: validation.error } as ApiResponse);
return;
}
const newValue = validation.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. Validierung erfolgt vor dem
// ersten Schreibzugriff, damit ein Bulk-PUT mit einem ungültigen
// Wert nicht die anderen Werte halb-committed liegen lässt.
const changes: Record<string, { von: unknown; nach: unknown }> = {};
const sanitizedEntries: Array<{ key: string; oldValue: string; newValue: string }> = [];
for (const [key, value] of Object.entries(settings)) {
const before = await prisma.appSetting.findUnique({ where: { key } });
const oldValue = before?.value ?? '-';
const stripped = appSettingService.sanitizeSettingValue(key, String(value));
const validation = appSettingService.validateSettingValue(key, stripped);
if (!validation.ok) {
res.status(400).json({ success: false, error: `${key}: ${validation.error}` } as ApiResponse);
return;
}
sanitizedEntries.push({ key, oldValue, newValue: validation.value });
}
for (const { key, oldValue, newValue } of sanitizedEntries) {
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);
}
}