Pentest 58.1 MEDIUM: documentType jetzt mit echter Whitelist-Validierung
Bisher lief documentType nur durch stripHtml – ein beliebiger String
("NICHT_ERLAUBT", "DROP TABLE ...", Tippfehler) wurde 1:1 als
ContractDocument.documentType in die DB geschrieben. Das brach
Frontend-Filter, Lieferbestätigung-Auto-Activation und Reports.
Neuer validateContractDocumentType-Helper in utils/sanitize:
- Whitelist ALLOWED_CONTRACT_DOCUMENT_TYPES (8 Werte, gespiegelt aus
Frontend CONTRACT_DOCUMENT_TYPES)
- Case-insensitiver Match, Rückgabe ist immer der kanonische Wert
- Wirft sprechende 400-Fehlermeldung mit Liste der erlaubten Werte
Eingesetzt in allen 3 Schreibpfaden:
- contract.controller.uploadContractDocument (multer-Datei wird bei
Reject sauber gelöscht)
- cachedEmail.controller.saveEmailAsContractDocument
- cachedEmail.controller.saveAttachmentAsContractDocument
Audit-Log + maybeActivateOnDeliveryConfirmation nutzen jetzt den
kanonischen Wert (statt der rohen Eingabe), damit Reports
einheitlich aussehen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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 } from '../utils/sanitize.js';
|
||||
import { sanitizeNotes, stripHtml, validateContractDocumentType } 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';
|
||||
@@ -1827,8 +1827,12 @@ export async function saveEmailAsContractDocument(req: AuthRequest, res: Respons
|
||||
if (!(await canAccessCachedEmail(req, res, emailId))) return;
|
||||
const { documentType, notes } = req.body;
|
||||
|
||||
if (!documentType || typeof documentType !== 'string') {
|
||||
res.status(400).json({ success: false, error: 'documentType ist erforderlich' } as ApiResponse);
|
||||
// Pentest 58.1: Whitelist-Validierung des documentType.
|
||||
let validatedType: string;
|
||||
try {
|
||||
validatedType = validateContractDocumentType(documentType);
|
||||
} catch (err) {
|
||||
res.status(400).json({ success: false, error: err instanceof Error ? err.message : 'Ungültiger documentType' } as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1872,21 +1876,20 @@ export async function saveEmailAsContractDocument(req: AuthRequest, res: Respons
|
||||
if (!fs.existsSync(uploadsDir)) fs.mkdirSync(uploadsDir, { recursive: true });
|
||||
|
||||
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
|
||||
const safeType = documentType.toLowerCase().replace(/[^a-z0-9]/g, '-');
|
||||
const safeType = validatedType.toLowerCase().replace(/[^a-z0-9]/g, '-');
|
||||
const newFilename = `${safeType}-email-${uniqueSuffix}.pdf`;
|
||||
const filePath = path.join(uploadsDir, newFilename);
|
||||
const relativePath = `/uploads/contract-documents/${newFilename}`;
|
||||
|
||||
const cleanType = stripHtml(documentType) as string;
|
||||
// Pentest 55.4: Lock vor Schreiben, damit parallele Requests blockieren.
|
||||
// PDF erst INNERHALB des Locks auf Disk schreiben → kein verwaister
|
||||
// Datei-Müll bei Race-Reject.
|
||||
const doc = await withContractDocumentLock(contract.id, cleanType, async () => {
|
||||
const doc = await withContractDocumentLock(contract.id, validatedType, async () => {
|
||||
fs.writeFileSync(filePath, pdfBuffer);
|
||||
return prisma.contractDocument.create({
|
||||
data: {
|
||||
contractId: contract.id,
|
||||
documentType: cleanType,
|
||||
documentType: validatedType,
|
||||
documentPath: relativePath,
|
||||
originalName: `${email.subject || 'email'}.pdf`,
|
||||
notes: sanitizeNotes(notes),
|
||||
@@ -1897,7 +1900,7 @@ 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;
|
||||
await maybeActivateOnDeliveryConfirmation(contract.id, documentType, req, deliveryDate);
|
||||
await maybeActivateOnDeliveryConfirmation(contract.id, validatedType, req, deliveryDate);
|
||||
|
||||
res.json({ success: true, data: doc } as ApiResponse);
|
||||
} catch (error) {
|
||||
@@ -2081,10 +2084,14 @@ export async function saveAttachmentAsContractDocument(req: AuthRequest, res: Re
|
||||
const filename = decodeURIComponent(req.params.filename);
|
||||
const { documentType, notes } = req.body;
|
||||
|
||||
if (!documentType || typeof documentType !== 'string') {
|
||||
// Pentest 58.1: Whitelist-Validierung des documentType.
|
||||
let validatedType: string;
|
||||
try {
|
||||
validatedType = validateContractDocumentType(documentType);
|
||||
} catch (err) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'documentType ist erforderlich',
|
||||
error: err instanceof Error ? err.message : 'Ungültiger documentType',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
@@ -2174,19 +2181,18 @@ export async function saveAttachmentAsContractDocument(req: AuthRequest, res: Re
|
||||
|
||||
const ext = path.extname(filename) || '.pdf';
|
||||
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
|
||||
const safeType = documentType.toLowerCase().replace(/[^a-z0-9]/g, '-');
|
||||
const safeType = validatedType.toLowerCase().replace(/[^a-z0-9]/g, '-');
|
||||
const newFilename = `${safeType}-${uniqueSuffix}${ext}`;
|
||||
const filePath = path.join(uploadsDir, newFilename);
|
||||
const relativePath = `/uploads/contract-documents/${newFilename}`;
|
||||
|
||||
const cleanType = stripHtml(documentType) as string;
|
||||
// Pentest 55.4: Lock vor Schreiben (siehe saveEmailAsContractDocument).
|
||||
const doc = await withContractDocumentLock(contract.id, cleanType, async () => {
|
||||
const doc = await withContractDocumentLock(contract.id, validatedType, async () => {
|
||||
fs.writeFileSync(filePath, attachment.content);
|
||||
return prisma.contractDocument.create({
|
||||
data: {
|
||||
contractId: contract.id,
|
||||
documentType: cleanType,
|
||||
documentType: validatedType,
|
||||
documentPath: relativePath,
|
||||
originalName: filename,
|
||||
notes: sanitizeNotes(notes),
|
||||
@@ -2197,7 +2203,7 @@ 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;
|
||||
await maybeActivateOnDeliveryConfirmation(contract.id, documentType, req, deliveryDate);
|
||||
await maybeActivateOnDeliveryConfirmation(contract.id, validatedType, req, deliveryDate);
|
||||
|
||||
res.json({ success: true, data: doc } as ApiResponse);
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user