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.
This commit is contained in:
2026-06-03 13:18:23 +02:00
parent 30f528596c
commit ec577e6d76
9 changed files with 186 additions and 8 deletions
+2 -1
View File
@@ -5,6 +5,7 @@ import fs from 'fs';
import * as contractController from '../controllers/contract.controller.js';
import * as invoiceController from '../controllers/invoice.controller.js';
import { authenticate, requirePermission } from '../middleware/auth.js';
import { scanUploadedPdfIfPresent } from '../middleware/pdfUploadSafety.js';
const router = Router();
@@ -54,7 +55,7 @@ router.post('/:id/invoices', authenticate, requirePermission('contracts:update')
// Vertragsdokumente
router.get('/:id/documents', authenticate, requirePermission('contracts:read'), contractController.getContractDocuments);
router.post('/:id/documents', authenticate, requirePermission('contracts:update'), docUpload.single('file'), contractController.uploadContractDocument);
router.post('/:id/documents', authenticate, requirePermission('contracts:update'), docUpload.single('file'), scanUploadedPdfIfPresent, contractController.uploadContractDocument);
router.delete('/:id/documents/:documentId', authenticate, requirePermission('contracts:update'), contractController.deleteContractDocument);
// Folgezähler
+2 -1
View File
@@ -3,6 +3,7 @@ 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 gdprController from '../controllers/gdpr.controller.js';
const router = Router();
@@ -84,7 +85,7 @@ router.get('/customer/:customerId/authorizations', requirePermission('customers:
router.post('/customer/:customerId/authorizations/:representativeId/send', requirePermission('customers:update'), gdprController.sendAuthorizationRequest);
router.post('/customer/:customerId/authorizations/:representativeId/grant', requirePermission('customers:update'), gdprController.grantAuthorization);
router.post('/customer/:customerId/authorizations/:representativeId/withdraw', requirePermission('customers:update'), gdprController.withdrawAuthorization);
router.post('/customer/:customerId/authorizations/:representativeId/upload', requirePermission('customers:update'), authUpload.single('document'), gdprController.uploadAuthorizationDocument);
router.post('/customer/:customerId/authorizations/:representativeId/upload', requirePermission('customers:update'), authUpload.single('document'), requireSafeUploadedPdf, gdprController.uploadAuthorizationDocument);
router.delete('/customer/:customerId/authorizations/:representativeId/document', requirePermission('customers:update'), gdprController.deleteAuthorizationDocument);
// Portal: Vollmachten
+2 -1
View File
@@ -3,6 +3,7 @@ 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();
@@ -34,7 +35,7 @@ router.use(authenticate);
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'), pdfTemplateController.createTemplate);
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);
+16 -1
View File
@@ -12,7 +12,8 @@ import {
canAccessBankCard,
canAccessIdentityDocument,
} from '../utils/accessControl.js';
import { validateOptionalIsoDate } from '../utils/sanitize.js';
import { validateOptionalIsoDate, assertSafePdf } from '../utils/sanitize.js';
import { ApiError } from '../utils/apiError.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
@@ -140,6 +141,20 @@ function validateUploadedFile(req: AuthRequest, res: Response, next: Function) {
return;
}
// Pentest 68.1 (LOW): PDF-Body auf aktive Inhalte scannen.
if (detected.mime === 'application/pdf') {
try {
const fullBuf = fs.readFileSync(req.file.path);
assertSafePdf(fullBuf);
} catch (e) {
try { fs.unlinkSync(req.file.path); } catch { /* ignore */ }
const status = e instanceof ApiError ? e.statusCode : 415;
const msg = e instanceof Error ? e.message : 'PDF ungültig';
res.status(status).json({ success: false, error: msg });
return;
}
}
// Filename auf kanonische Extension normalisieren. Multer hat
// `<unique>.gif.php` o.ä. geschrieben wir wollen `<unique>.gif`.
const dir = path.dirname(req.file.path);