fix(security): Ghostscript mit -dSAFER, Schutz vor PDF-Dekompressionsbomben

- Ghostscript läuft auf PDFs aus Mails und vom Scanner jetzt mit -dSAFER
  statt -dNOSAFER (kein Datei-/Prozesszugriff aus PostScript), mit
  Timeout (60 s je Seite, 10 min je Dokument, SIGKILL) und höchstens
  500 gerenderten Seiten. Temp-Dateien werden auch im Fehlerfall
  entfernt. Ungenutztes getPageCount (brauchte Dateizugriff) entfernt.
- Neuer Vorab-Check ueberschreitetDekompressionsLimit (256 MB, streamend,
  erkennt auch Deflate-Ketten und abgeschnittene Streams). Fingerprint
  (pdf.js) und E-Rechnungs-Extraktion (pdf-lib) laden Bomben nicht mehr;
  eine einzelne Mail konnte das Backend sonst per OOM dauerhaft lahmlegen.
- Verschlüsselte PDFs bekommen keinen Inhalts-Fingerprint mehr ('' =
  nicht vergleichbar), weil pdf.js sie erst intern entschlüsselt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bjoernpoettkerandClaude Opus 5.5 committed 2026-11-04 10:46:43 +01:00
1 parent bb8bf3714f
commit de9cb09bcd
7 files changed
+355 -43

No files matched your search

@@ -49,3 +49,24 @@ describe('extrahiereEingebetteteDateien', () => {
expect(await extrahiereEingebetteteDateien(pdf)).toEqual([]);
});
});
describe('extrahiereEingebetteteDateien – Dekompressionsbombe', () => {
it('lädt ein PDF mit Deflate-Bombe gar nicht erst', async () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const zlib = require('zlib') as typeof import('zlib');
const bombe = zlib.deflateSync(Buffer.alloc(300 * 1024 * 1024));
const pdf = Buffer.concat([
Buffer.from(
`%PDF-1.7\n1 0 obj\n<< /Length ${bombe.length} /Filter /FlateDecode >>\nstream\n`,
'latin1',
),
bombe,
Buffer.from('\nendstream\nendobj\n%%EOF\n', 'latin1'),
]);
const load = jest.spyOn(PDFDocument, 'load');
await expect(extrahiereEingebetteteDateien(pdf)).resolves.toEqual([]);
expect(load).not.toHaveBeenCalled();
load.mockRestore();
});
});