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

@@ -1,5 +1,9 @@
import * as crypto from 'crypto';
import { PDFParse } from 'pdf-parse';
import {
istVerschluesselt,
ueberschreitetDekompressionsLimit,
} from '../common/pdf-dekompression.util';
/**
* Unterhalb dieser Textlänge gilt ein PDF als "ohne Textebene" (z.B. Scan) –
@@ -15,11 +19,21 @@ const MIN_TEXT_LENGTH = 20;
*
* @returns Fingerprint (64 Hex-Zeichen), `''` bei PDFs ohne verwertbaren Text
* (bereits berechnet, aber nicht vergleichbar) oder `null`, wenn das
* PDF nicht gelesen werden konnte.
* PDF nicht gelesen werden konnte. Verschlüsselte PDFs und solche,
* die entpackt zu groß wären, liefern ebenfalls `''`.
*/
export async function computeContentFingerprint(
pdf: Buffer,
): Promise<string | null> {
// pdf.js entpackt ohne Limit – Dekompressionsbomben gar nicht erst laden.
// Verschlüsselte Streams entschlüsselt pdf.js erst intern; vorab sind sie
// nicht prüfbar. Beides gilt als "nicht vergleichbar" ('').
if (
istVerschluesselt(pdf) ||
(await ueberschreitetDekompressionsLimit(pdf))
) {
return '';
}
// pdf.js übernimmt den Puffer, deshalb eine Kopie übergeben
const parser = new PDFParse({ data: new Uint8Array(pdf) });
try {