Pentest 24.6 INFO + 26.7 LOW: PENDING-Status sperren + documentPath-Validator

24.6 (Portal kann Consent auf PENDING zurücksetzen):
- gdpr.controller updateCustomerConsent prüft jetzt explizit, dass
  der Portal-User nur GRANTED oder WITHDRAWN setzen kann. PENDING
  ist nur der initiale System-Status; ein Reset darauf hätte die
  DSGVO-Auswertung verfälscht.

26.7 (documentPath ohne Validierung):
- Neuer Helper isValidDocumentPath + assertValidDocumentPath in
  utils/sanitize: nur /?uploads/<safe>, keine "..", keine
  javascript:/data:/vbscript:, kein HTML.
- consent.service.updateConsent ruft den Assert auf – Defense-in-
  Depth gegen zukünftige Caller, die documentPath aus User-Input
  durchreichen könnten.
- authorization.service.grantAuthorization analog.
- Cleanup-Skript (prisma/cleanup-xss-and-mass-assignment) entfernt
  seine lokale Kopie der Path-Validierung und nutzt den shared
  Helper – Single Source of Truth.

27.1 (Altdaten in Staging-DB):
- Cleanup-Skript läuft sowieso bei jedem Container-Start. Nina-
  Records mit "../../../etc/passwd" werden beim nächsten Restart
  genullt (oder verschwinden mit dem VM-Snapshot-Wechsel).

Live-Test isValidDocumentPath: 13/13 OK – legitime Pfade durch,
Traversal/JS-URI/HTML blockiert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-02 14:20:13 +02:00
parent ad81a7c93e
commit 25681075b4
5 changed files with 48 additions and 13 deletions
@@ -309,6 +309,18 @@ export async function updateCustomerConsent(req: AuthRequest, res: Response) {
return res.status(400).json({ success: false, error: 'Ungültiger Consent-Typ' });
}
// Pentest 24.6 (INFO, 2026-06-02): Portal-User durfte `PENDING`
// mitschicken und damit den Consent-Status auf den initialen System-
// Status zurücksetzen. PENDING ist nur intern (Default beim
// Customer-Anlegen); Portal darf nur GRANTED oder WITHDRAWN setzen.
// Verfälschte sonst die DSGVO-Auswertung.
if (status !== 'GRANTED' && status !== 'WITHDRAWN') {
return res.status(400).json({
success: false,
error: 'Portal-Einwilligungen dürfen nur auf GRANTED oder WITHDRAWN gesetzt werden.',
});
}
const consentLabels: Record<string, string> = {
DATA_PROCESSING: 'Datenverarbeitung',
MARKETING_EMAIL: 'E-Mail-Marketing',
@@ -1,6 +1,7 @@
import prisma from '../lib/prisma.js';
import fs from 'fs';
import path from 'path';
import { assertValidDocumentPath } from '../utils/sanitize.js';
/**
* Vollmachten für einen Kunden abrufen (wer darf diesen Kunden einsehen?)
@@ -53,6 +54,8 @@ export async function grantAuthorization(
representativeId: number,
data: { source?: string; documentPath?: string; notes?: string }
) {
// Pentest 26.7 (Defense-in-Depth): documentPath nur als /uploads/<safe>.
assertValidDocumentPath(data.documentPath, 'documentPath');
return prisma.representativeAuthorization.upsert({
where: {
customerId_representativeId: { customerId, representativeId },
+7
View File
@@ -2,6 +2,7 @@ import { ConsentType, ConsentStatus } from '@prisma/client';
import prisma from '../lib/prisma.js';
import fs from 'fs';
import path from 'path';
import { assertValidDocumentPath } from '../utils/sanitize.js';
// Whitelist legitimer Werte für CustomerConsent.source. Schema-Kommentar:
// "portal", "telefon", "papier", "email". Public-Link-Flow nutzt
@@ -80,6 +81,12 @@ export async function updateConsent(
throw new Error('Kunde nicht gefunden');
}
// Pentest 26.7: documentPath darf nur ein gültiger /uploads/<safe>-Pfad
// sein. Aktuell hat KEIN Endpoint diesen Wert aus User-Input gemappt
// (Portal: nicht aus Body, Admin-Auth-Upload: server-generated). Diese
// Service-Side-Validation ist Defense-in-Depth gegen zukünftige Caller.
assertValidDocumentPath(data.documentPath, 'documentPath');
const now = new Date();
const updateData = {
status: data.status,
+21
View File
@@ -225,6 +225,27 @@ export function validateContractDocumentType(raw: unknown): string {
return canonical;
}
// Pentest 26.7 LOW (defense-in-depth, 2026-06-02): documentPath wird
// (außer beim Upload-Endpoint) NIE direkt aus User-Input übernommen.
// Falls doch jemand auf die Idee kommt, das Feld irgendwo zu mappen,
// fangen wir hier Path-Traversal / Javascript-URIs / HTML ab.
// Spiegelt isValidDocumentPath aus prisma/cleanup-xss-and-mass-assignment.ts
// 1:1 Single Source of Truth für Lese- UND Schreibpfad.
export function isValidDocumentPath(v: string | null | undefined): boolean {
if (!v) return true; // null/leer ist OK Feld bleibt einfach unbesetzt
if (typeof v !== 'string') return false;
if (v.includes('..')) return false;
if (/(?:javascript|data|vbscript)\s*:/i.test(v)) return false;
if (/<[a-z!/]/i.test(v)) return false;
return /^\/?uploads\/[A-Za-z0-9._\-/]+$/.test(v);
}
export function assertValidDocumentPath(v: string | null | undefined, fieldLabel = 'documentPath'): void {
if (!isValidDocumentPath(v)) {
throw new Error(`${fieldLabel} ist kein gültiger Upload-Pfad (erlaubt: /uploads/<safe>).`);
}
}
// Pentest 51.3 + 60.3 (MEDIUM, 2026-06-01): Telefon-/Vorwahl-Felder
// dürfen NIE CRLF oder andere Control-Chars enthalten sonst sind sie
// ein Header-Injection-Vektor (Mail, HTTP), wenn der Wert mal in einen