import * as zlib from 'zlib'; import { PDFDocument } from 'pdf-lib'; import { istVerschluesselt, ueberschreitetDekompressionsLimit, } from './pdf-dekompression.util'; const MB = 1024 * 1024; /** Minimales PDF-Gerüst mit einem Stream – für den Scanner reicht das. */ function pdfMitStream(daten: Buffer, filter = '/FlateDecode'): Buffer { return Buffer.concat([ Buffer.from( `%PDF-1.7\n1 0 obj\n<< /Length ${daten.length} /Filter ${filter} >>\nstream\n`, 'latin1', ), daten, Buffer.from('\nendstream\nendobj\n%%EOF\n', 'latin1'), ]); } describe('ueberschreitetDekompressionsLimit', () => { it('lässt ein normales PDF durch', async () => { const doc = await PDFDocument.create(); doc.addPage().drawText('Rechnung 2026-0001'); const pdf = Buffer.from(await doc.save()); expect(await ueberschreitetDekompressionsLimit(pdf, 1 * MB)).toBe(false); }); it('erkennt eine Deflate-Bombe (wenige KB → viele MB)', async () => { const bombe = zlib.deflateSync(Buffer.alloc(20 * MB)); expect(bombe.length).toBeLessThan(100 * 1024); expect( await ueberschreitetDekompressionsLimit(pdfMitStream(bombe), 5 * MB), ).toBe(true); }); it('erkennt verschachtelte Deflate-Ketten', async () => { const innen = zlib.deflateSync(Buffer.alloc(20 * MB)); const aussen = zlib.deflateSync(innen); const pdf = pdfMitStream(aussen, '[/FlateDecode /FlateDecode]'); expect(await ueberschreitetDekompressionsLimit(pdf, 5 * MB)).toBe(true); }); it('summiert über mehrere Streams', async () => { const teil = zlib.deflateSync(Buffer.alloc(2 * MB)); const pdf = Buffer.concat([1, 2, 3, 4].map(() => pdfMitStream(teil))); expect(await ueberschreitetDekompressionsLimit(pdf, 5 * MB)).toBe(true); expect(await ueberschreitetDekompressionsLimit(pdf, 10 * MB)).toBe(false); }); it('zählt auch abgeschnittene Streams (pdf.js liest sie trotzdem)', async () => { const bombe = zlib.deflateSync(Buffer.alloc(20 * MB)); const abgeschnitten = bombe.subarray(0, bombe.length - 10); expect( await ueberschreitetDekompressionsLimit( pdfMitStream(abgeschnitten), 5 * MB, ), ).toBe(true); }); it('wertet nicht komprimierte Streams mit ihrer Rohgröße', async () => { const roh = Buffer.alloc(2 * MB, 0x41); expect( await ueberschreitetDekompressionsLimit( pdfMitStream(roh, '/DCTDecode'), 1 * MB, ), ).toBe(true); }); }); describe('istVerschluesselt', () => { it('erkennt ein /Encrypt-Dictionary im Trailer', () => { const pdf = Buffer.from( '%PDF-1.7\ntrailer\n<< /Encrypt 5 0 R >>\n', 'latin1', ); expect(istVerschluesselt(pdf)).toBe(true); }); it('ignoriert ähnliche Namen', () => { const pdf = Buffer.from( '%PDF-1.7\n<< /EncryptMetadata false >>\n', 'latin1', ); expect(istVerschluesselt(pdf)).toBe(false); }); });