- 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>
73 lines
2.3 KiB
TypeScript
73 lines
2.3 KiB
TypeScript
import { PDFDocument } from 'pdf-lib';
|
||
import { extrahiereEingebetteteDateien } from './pdf-attachment.util';
|
||
|
||
const XML = '<?xml version="1.0"?><rsm:CrossIndustryInvoice/>';
|
||
|
||
async function pdfMitAnhang(
|
||
...anhaenge: { name: string; inhalt: string }[]
|
||
): Promise<Buffer> {
|
||
const doc = await PDFDocument.create();
|
||
doc.addPage();
|
||
for (const a of anhaenge) {
|
||
await doc.attach(Buffer.from(a.inhalt, 'utf8'), a.name, {
|
||
mimeType: 'text/xml',
|
||
});
|
||
}
|
||
return Buffer.from(await doc.save());
|
||
}
|
||
|
||
describe('extrahiereEingebetteteDateien', () => {
|
||
it('liest einen eingebetteten ZUGFeRD-Anhang aus', async () => {
|
||
const pdf = await pdfMitAnhang({ name: 'factur-x.xml', inhalt: XML });
|
||
|
||
const dateien = await extrahiereEingebetteteDateien(pdf);
|
||
|
||
expect(dateien).toHaveLength(1);
|
||
expect(dateien[0].name).toBe('factur-x.xml');
|
||
expect(dateien[0].inhalt.toString('utf8')).toBe(XML);
|
||
});
|
||
|
||
it('liefert jeden Anhang nur einmal, obwohl er in /Names und /AF steht', async () => {
|
||
const pdf = await pdfMitAnhang(
|
||
{ name: 'xrechnung.xml', inhalt: XML },
|
||
{ name: 'anhang.txt', inhalt: 'Lieferschein' },
|
||
);
|
||
|
||
const dateien = await extrahiereEingebetteteDateien(pdf);
|
||
|
||
expect(dateien.map((d) => d.name).sort()).toEqual([
|
||
'anhang.txt',
|
||
'xrechnung.xml',
|
||
]);
|
||
});
|
||
|
||
it('liefert eine leere Liste für ein PDF ohne Anhänge', async () => {
|
||
const doc = await PDFDocument.create();
|
||
doc.addPage();
|
||
const pdf = Buffer.from(await doc.save());
|
||
|
||
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();
|
||
});
|
||
});
|