Files
opencrm/backend/src/routes/pdfTemplate.routes.ts
T
duffyduck ec577e6d76 Pentest 68.1 (LOW) + 68.2 (INFO): PDF-Active-Content-Filter + Modal-Limit
68.1: Magic-Byte-Check prüfte nur %PDF-. PDFs mit /JavaScript, /JS,
/Launch, /EmbeddedFile, /RichMedia (Flash) kamen durch und wurden
inline ausgeliefert – Browser-Viewer ignorieren JS, Adobe Acrobat
nicht.

- Neuer Helper assertSafePdf(buf) in utils/sanitize.ts mit
  case-sensitivem String-Scan auf die fünf Action-Patterns
  (\b-Word-Boundary verhindert False-Positives bei /JSXForm etc.).
- Neue Middleware pdfUploadSafety.ts mit zwei Varianten:
  requireSafeUploadedPdf (PDF-only) und scanUploadedPdfIfPresent
  (durchwinkt JPG/PNG, scannt nur PDFs).
- Eingehängt in: upload.routes (Magic-Byte-Validator erweitert),
  gdpr.routes Vollmacht-Upload, pdfTemplate.routes Template-Upload,
  contract.routes Vertragsdokumente, cachedEmail.controller
  (saveAttachmentTo, saveAttachmentAsInvoice,
  saveAttachmentAsContractDocument).
- Inline-Vorschau bleibt – Pentester-Empfehlung "disposition=inline
  abschalten" wurde bewusst nicht umgesetzt (löst Acrobat-Risiko
  nicht, bricht aber ~20 UI-Stellen).
- Smoke-Test: 5 Payload-Typen abgelehnt, clean PDF + Non-PDF + JSXForm
  durchgewinkt.

68.2: JpgToPdfModal-Self-DoS – MAX_IMAGES=50, MAX_IMAGE_BYTES=25MB.
2026-06-03 13:18:23 +02:00

55 lines
2.4 KiB
TypeScript

import { Router } from 'express';
import multer from 'multer';
import path from 'path';
import fs from 'fs';
import { authenticate, requirePermission } from '../middleware/auth.js';
import { requireSafeUploadedPdf } from '../middleware/pdfUploadSafety.js';
import * as pdfTemplateController from '../controllers/pdfTemplate.controller.js';
const router = Router();
// Upload-Verzeichnis
const templatesDir = path.join(process.cwd(), 'uploads', 'pdf-templates');
if (!fs.existsSync(templatesDir)) {
fs.mkdirSync(templatesDir, { recursive: true });
}
const upload = multer({
storage: multer.diskStorage({
destination: (_req, _file, cb) => cb(null, templatesDir),
filename: (_req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
cb(null, `template-${uniqueSuffix}${path.extname(file.originalname)}`);
},
}),
fileFilter: (_req, file, cb) => {
if (file.mimetype === 'application/pdf') cb(null, true);
else cb(new Error('Nur PDF-Dateien sind erlaubt'));
},
limits: { fileSize: 20 * 1024 * 1024 },
});
router.use(authenticate);
// CRUD
router.get('/', requirePermission('settings:read'), pdfTemplateController.getTemplates);
router.get('/crm-fields', requirePermission('settings:read'), pdfTemplateController.getCrmFields);
router.get('/:id', requirePermission('settings:read'), pdfTemplateController.getTemplate);
router.post('/', requirePermission('settings:update'), upload.single('template'), requireSafeUploadedPdf, pdfTemplateController.createTemplate);
router.put('/:id', requirePermission('settings:update'), pdfTemplateController.updateTemplate);
router.delete('/:id', requirePermission('settings:update'), pdfTemplateController.deleteTemplate);
// PDF-Felder auslesen
router.get('/:id/fields', requirePermission('settings:read'), pdfTemplateController.extractFields);
// Annotierte Vorschau (Feldnamen in der PDF sichtbar)
router.get('/:id/preview', requirePermission('settings:read'), pdfTemplateController.getAnnotatedPreview);
// PDF generieren
router.get('/:id/generate/:contractId/inputs', requirePermission('contracts:read'), pdfTemplateController.getRequiredInputs);
router.post('/:id/generate/:contractId', requirePermission('contracts:read'), pdfTemplateController.generatePdf);
// Auch GET für direkte Links (ohne Extras)
router.get('/:id/generate/:contractId', requirePermission('contracts:read'), pdfTemplateController.generatePdf);
export default router;