diff --git a/backend/src/controllers/cachedEmail.controller.ts b/backend/src/controllers/cachedEmail.controller.ts index b57eb130..8a0e4446 100644 --- a/backend/src/controllers/cachedEmail.controller.ts +++ b/backend/src/controllers/cachedEmail.controller.ts @@ -8,7 +8,7 @@ import { sendEmail, SmtpCredentials, SendEmailParams, EmailAttachment } from '.. import { fetchAttachment, appendToSent, ImapCredentials } from '../services/imapService.js'; import { getImapSmtpSettings } from '../services/emailProvider/emailProviderService.js'; import { decrypt } from '../utils/encryption.js'; -import { sanitizeNotes, stripHtml, validateContractDocumentType } from '../utils/sanitize.js'; +import { sanitizeNotes, stripHtml, validateContractDocumentType, validateOptionalIsoDate } from '../utils/sanitize.js'; import { logChange } from '../services/audit.service.js'; import { ApiResponse, AuthRequest } from '../types/index.js'; import { getCustomerTargets, getContractTargets, getIdentityDocumentTargets, getBankCardTargets, documentTargets } from '../config/documentTargets.config.js'; @@ -1836,6 +1836,15 @@ export async function saveEmailAsContractDocument(req: AuthRequest, res: Respons return; } + // Pentest 62.7: deliveryDate früh validieren, bevor wir Dateien schreiben. + let deliveryDate: string | null; + try { + deliveryDate = validateOptionalIsoDate(req.body?.deliveryDate, 'deliveryDate'); + } catch (err) { + res.status(400).json({ success: false, error: err instanceof Error ? err.message : 'Ungültiges Lieferdatum' } as ApiResponse); + return; + } + const email = await cachedEmailService.getCachedEmailById(emailId); if (!email) { res.status(404).json({ success: false, error: 'E-Mail nicht gefunden' } as ApiResponse); @@ -1898,8 +1907,8 @@ export async function saveEmailAsContractDocument(req: AuthRequest, res: Respons }); }); - // Falls Lieferbestätigung: DRAFT → ACTIVE + startDate setzen falls leer - const deliveryDate = typeof req.body?.deliveryDate === 'string' ? req.body.deliveryDate : null; + // Falls Lieferbestätigung: DRAFT → ACTIVE + startDate setzen falls leer. + // deliveryDate wurde oben schon validiert (Pentest 62.7). await maybeActivateOnDeliveryConfirmation(contract.id, validatedType, req, deliveryDate); res.json({ success: true, data: doc } as ApiResponse); @@ -2096,6 +2105,15 @@ export async function saveAttachmentAsContractDocument(req: AuthRequest, res: Re return; } + // Pentest 62.7: deliveryDate früh validieren, bevor wir Dateien schreiben. + let deliveryDate: string | null; + try { + deliveryDate = validateOptionalIsoDate(req.body?.deliveryDate, 'deliveryDate'); + } catch (err) { + res.status(400).json({ success: false, error: err instanceof Error ? err.message : 'Ungültiges Lieferdatum' } as ApiResponse); + return; + } + const email = await cachedEmailService.getCachedEmailById(emailId); if (!email) { res.status(404).json({ success: false, error: 'E-Mail nicht gefunden' } as ApiResponse); @@ -2201,8 +2219,8 @@ export async function saveAttachmentAsContractDocument(req: AuthRequest, res: Re }); }); - // Falls Lieferbestätigung: DRAFT → ACTIVE + startDate setzen falls leer - const deliveryDate = typeof req.body?.deliveryDate === 'string' ? req.body.deliveryDate : null; + // Falls Lieferbestätigung: DRAFT → ACTIVE + startDate setzen falls leer. + // deliveryDate wurde oben schon validiert (Pentest 62.7). await maybeActivateOnDeliveryConfirmation(contract.id, validatedType, req, deliveryDate); res.json({ success: true, data: doc } as ApiResponse); diff --git a/backend/src/controllers/contract.controller.ts b/backend/src/controllers/contract.controller.ts index 71aab25e..6cb509f2 100644 --- a/backend/src/controllers/contract.controller.ts +++ b/backend/src/controllers/contract.controller.ts @@ -8,7 +8,7 @@ import * as authorizationService from '../services/authorization.service.js'; import { recordPredecessorFinalReading } from '../services/customer.service.js'; import { ApiResponse, AuthRequest } from '../types/index.js'; import { logChange } from '../services/audit.service.js'; -import { sanitizeContract, sanitizeContractStrict, sanitizeContracts, sanitizeContractsStrict, stripHtml, sanitizeNotes, validateContractDocumentType } from '../utils/sanitize.js'; +import { sanitizeContract, sanitizeContractStrict, sanitizeContracts, sanitizeContractsStrict, stripHtml, sanitizeNotes, validateContractDocumentType, validateOptionalIsoDate } from '../utils/sanitize.js'; import { canAccessContract } from '../utils/accessControl.js'; import { maybeActivateOnDeliveryConfirmation, withContractDocumentLock } from '../services/contractStatusScheduler.service.js'; @@ -722,7 +722,7 @@ export async function uploadContractDocument(req: AuthRequest, res: Response): P try { const contractId = parseInt(req.params.id); if (!(await canAccessContract(req, res, contractId))) return; - const { documentType, notes, deliveryDate } = req.body; + const { documentType, notes } = req.body; if (!req.file) { res.status(400).json({ success: false, error: 'Keine Datei hochgeladen' } as ApiResponse); @@ -734,6 +734,16 @@ export async function uploadContractDocument(req: AuthRequest, res: Response): P return; } + // Pentest 62.7: deliveryDate validieren (ISO-8601 oder null). + let deliveryDate: string | null; + try { + deliveryDate = validateOptionalIsoDate(req.body?.deliveryDate, 'deliveryDate'); + } catch (err) { + try { fs.unlinkSync(req.file.path); } catch { /* ignore */ } + res.status(400).json({ success: false, error: err instanceof Error ? err.message : 'Ungültiges Lieferdatum' } as ApiResponse); + return; + } + const documentPath = `/uploads/contract-documents/${req.file.filename}`; // Pentest 58.1: Whitelist-Validierung statt nur stripHtml. Multer hat // die Datei schon geschrieben – bei Reject räumen wir sie wieder weg. diff --git a/backend/src/routes/upload.routes.ts b/backend/src/routes/upload.routes.ts index 452746c5..1cf973a8 100644 --- a/backend/src/routes/upload.routes.ts +++ b/backend/src/routes/upload.routes.ts @@ -12,6 +12,7 @@ import { canAccessBankCard, canAccessIdentityDocument, } from '../utils/accessControl.js'; +import { validateOptionalIsoDate } from '../utils/sanitize.js'; // Pentest 56.1 (HIGH, 2026-06-01): Upload-Endpoints prüften nur die // Permission, nicht ob die Ziel-Resource zum Caller passt. Helper-Funktion @@ -738,11 +739,18 @@ async function handleContractDocumentUpload( const dateField = fieldName === 'cancellationConfirmationPath' ? 'cancellationConfirmationDate' : 'cancellationConfirmationOptionsDate'; - const provided = typeof req.body?.confirmationDate === 'string' ? req.body.confirmationDate : null; + // Pentest 62.7: confirmationDate gegen ISO-8601 validieren. + let provided: string | null; + try { + provided = validateOptionalIsoDate(req.body?.confirmationDate, 'confirmationDate'); + } catch (err) { + cleanupFile(req.file?.path); + res.status(400).json({ success: false, error: err instanceof Error ? err.message : 'Ungültiges Bestätigungsdatum' }); + return; + } let target: Date | null = null; if (provided) { - const parsed = new Date(provided); - if (!isNaN(parsed.getTime())) target = parsed; + target = new Date(provided); } if (target) { updateData[dateField] = target; diff --git a/backend/src/utils/sanitize.ts b/backend/src/utils/sanitize.ts index a458b79f..768608a7 100644 --- a/backend/src/utils/sanitize.ts +++ b/backend/src/utils/sanitize.ts @@ -246,6 +246,33 @@ export function sanitizePhoneField(raw: unknown, fieldLabel: string): string | u return trimmed; } +// Pentest 62.7 (LOW, 2026-06-02): deliveryDate/confirmationDate-Felder +// liefen ungeprüft in maybeActivateOnDeliveryConfirmation. XSS-Payloads +// gingen mit 200 durch, weil das ungültige Datum nur silent als null +// behandelt wurde. Impact gering, aber API-Hygiene: ungültige Eingabe +// soll 400 zurückgeben, nicht 200. +// +// Akzeptiert: ISO-8601-Datum (YYYY-MM-DD) und Datum+Zeit (mit oder ohne +// Zeitzone). Whitespace wird getrimmt. null / leerer String / undefined +// sind OK – der Aufrufer behandelt das als "Datum nicht gesetzt". +const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:?\d{2})?)?$/; +export function validateOptionalIsoDate(raw: unknown, fieldLabel: string): string | null { + if (raw == null) return null; + if (typeof raw !== 'string') { + throw new Error(`${fieldLabel} muss ein Datums-String (YYYY-MM-DD) sein.`); + } + const trimmed = raw.trim(); + if (trimmed === '') return null; + if (!ISO_DATE_REGEX.test(trimmed)) { + throw new Error(`${fieldLabel} muss ISO-8601-Format haben (YYYY-MM-DD oder YYYY-MM-DDTHH:MM:SS).`); + } + const parsed = new Date(trimmed); + if (isNaN(parsed.getTime())) { + throw new Error(`${fieldLabel} ist kein gültiges Datum.`); + } + return trimmed; +} + const NOTES_DEFAULT_MAX = 2000; export function sanitizeNotes(raw: unknown, maxLength: number = NOTES_DEFAULT_MAX): string | null { if (raw == null) return null;