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:
@@ -9,7 +9,7 @@
|
|||||||
* mehrfach aufrufbar.
|
* mehrfach aufrufbar.
|
||||||
*/
|
*/
|
||||||
import prisma from '../src/lib/prisma.js';
|
import prisma from '../src/lib/prisma.js';
|
||||||
import { stripHtml } from '../src/utils/sanitize.js';
|
import { stripHtml, isValidDocumentPath as isValidDocumentPathShared } from '../src/utils/sanitize.js';
|
||||||
import { ALLOWED_SETTING_KEYS } from '../src/services/appSetting.service.js';
|
import { ALLOWED_SETTING_KEYS } from '../src/services/appSetting.service.js';
|
||||||
|
|
||||||
const CUSTOMER_STRING_FIELDS = [
|
const CUSTOMER_STRING_FIELDS = [
|
||||||
@@ -93,18 +93,10 @@ const ALLOWED_CONSENT_SOURCES = new Set([
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// Legitimer documentPath: relativer Pfad unter uploads/, keine ".."-Segmente.
|
// Legitimer documentPath: relativer Pfad unter uploads/, keine ".."-Segmente.
|
||||||
// Schreibend werden Pfade ausschließlich vom multer-Upload erzeugt
|
// Pentest 27.1 (2026-05-20). Helper jetzt zentral in utils/sanitize – wir
|
||||||
// (server-kontrollierter Dateiname), bestehende Pentest-Hinterlassenschaften
|
// re-exportieren hier nur, damit der bestehende Code keinen Schaden nimmt
|
||||||
// wie "../../../etc/passwd" oder "javascript:alert(1)" müssen aus der DB
|
// und wir nur EINE Quelle der Wahrheit pflegen müssen.
|
||||||
// raus (Pentest 2026-05-20 LOW 27.1).
|
const isValidDocumentPath = isValidDocumentPathShared;
|
||||||
function isValidDocumentPath(v: string | null | undefined): boolean {
|
|
||||||
if (!v) return true; // null/leer ist OK
|
|
||||||
if (v.includes('..')) return false;
|
|
||||||
if (/(?:javascript|data|vbscript)\s*:/i.test(v)) return false;
|
|
||||||
if (/<[a-z!\/]/i.test(v)) return false; // HTML im Pfad
|
|
||||||
// erlaubt: "uploads/...", "/uploads/..."; keine Kontrollzeichen
|
|
||||||
return /^\/?uploads\/[A-Za-z0-9._\-\/]+$/.test(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function cleanupConsents() {
|
async function cleanupConsents() {
|
||||||
// version + documentPath: HTML strippen (waren ohne Validierung).
|
// version + documentPath: HTML strippen (waren ohne Validierung).
|
||||||
|
|||||||
@@ -309,6 +309,18 @@ export async function updateCustomerConsent(req: AuthRequest, res: Response) {
|
|||||||
return res.status(400).json({ success: false, error: 'Ungültiger Consent-Typ' });
|
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> = {
|
const consentLabels: Record<string, string> = {
|
||||||
DATA_PROCESSING: 'Datenverarbeitung',
|
DATA_PROCESSING: 'Datenverarbeitung',
|
||||||
MARKETING_EMAIL: 'E-Mail-Marketing',
|
MARKETING_EMAIL: 'E-Mail-Marketing',
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import prisma from '../lib/prisma.js';
|
import prisma from '../lib/prisma.js';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import { assertValidDocumentPath } from '../utils/sanitize.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vollmachten für einen Kunden abrufen (wer darf diesen Kunden einsehen?)
|
* Vollmachten für einen Kunden abrufen (wer darf diesen Kunden einsehen?)
|
||||||
@@ -53,6 +54,8 @@ export async function grantAuthorization(
|
|||||||
representativeId: number,
|
representativeId: number,
|
||||||
data: { source?: string; documentPath?: string; notes?: string }
|
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({
|
return prisma.representativeAuthorization.upsert({
|
||||||
where: {
|
where: {
|
||||||
customerId_representativeId: { customerId, representativeId },
|
customerId_representativeId: { customerId, representativeId },
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { ConsentType, ConsentStatus } from '@prisma/client';
|
|||||||
import prisma from '../lib/prisma.js';
|
import prisma from '../lib/prisma.js';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import { assertValidDocumentPath } from '../utils/sanitize.js';
|
||||||
|
|
||||||
// Whitelist legitimer Werte für CustomerConsent.source. Schema-Kommentar:
|
// Whitelist legitimer Werte für CustomerConsent.source. Schema-Kommentar:
|
||||||
// "portal", "telefon", "papier", "email". Public-Link-Flow nutzt
|
// "portal", "telefon", "papier", "email". Public-Link-Flow nutzt
|
||||||
@@ -80,6 +81,12 @@ export async function updateConsent(
|
|||||||
throw new Error('Kunde nicht gefunden');
|
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 now = new Date();
|
||||||
const updateData = {
|
const updateData = {
|
||||||
status: data.status,
|
status: data.status,
|
||||||
|
|||||||
@@ -225,6 +225,27 @@ export function validateContractDocumentType(raw: unknown): string {
|
|||||||
return canonical;
|
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
|
// Pentest 51.3 + 60.3 (MEDIUM, 2026-06-01): Telefon-/Vorwahl-Felder
|
||||||
// dürfen NIE CRLF oder andere Control-Chars enthalten – sonst sind sie
|
// dürfen NIE CRLF oder andere Control-Chars enthalten – sonst sind sie
|
||||||
// ein Header-Injection-Vektor (Mail, HTTP), wenn der Wert mal in einen
|
// ein Header-Injection-Vektor (Mail, HTTP), wenn der Wert mal in einen
|
||||||
|
|||||||
Reference in New Issue
Block a user