Skip to content

Flate, LZW and Brotli decode with no bound on output size (decompression-bomb DoS) #1435

Description

@KHDHaDi

Flate, LZW and Brotli decode with no bound on output size (decompression-bomb DoS)

The problem

None of the three decompressing filters limits how many bytes it produces. Each decodes a whole stream into memory and stops only at the end of input or the ~2 GB array limit. A small crafted stream can drive a single Decode call to allocate up to ~2 GB, and two things make it worse:

  • Filter chains. /Filter [/FlateDecode /FlateDecode] is decoded stage by stage, so the ratios multiply. This triggers on reading the file, before any page is rendered.
  • No aggregate limit. Nothing budgets total decoded bytes across a document.

This is CWE-409. Impact is availability only: an untrusted PDF can exhaust memory and crash the process.

Where (against upstream/master)

I went through every filter. Two different bomb classes exist.

Ratio bombs — small input, large output, unbounded:

  • FlateFilter.cs — output grows in an unbounded MemoryStream. Up to ~1032:1 per stage, and a filter chain multiplies it.
  • LzwFilter.cs — unbounded output MemoryStream; the dictionary also grows without a cap.
  • BrotliFilter.cs — buffer doubles up to 0x7FFFFFC7, no cap below the array limit. Highest ratio per byte.
  • RunLengthFilter.cs — unbounded ArrayPoolBufferWriter; a run byte expands two input bytes to up to 128 output bytes (~64:1). Modest, but real and chainable.

Allocation bomb — huge buffer from attacker-declared dimensions, independent of input size:

  • CcittFaxDecodeFilter.cs — allocates new byte[(cols+7)/8 * rows] from /Columns and /Rows//Height in the dictionary. A few bytes of input with large declared dimensions force a large up-front allocation.

Not a bomb source (no change needed):

  • Ascii85Filter.cs, AsciiHexDecodeFilter.cs — output is smaller than input; they only carry a bomb if a real bomb follows them in the chain.
  • DctDecodeFilter.cs, Jbig2DecodeFilter.cs, JpxDecodeFilter.cs — unsupported, they throw before allocating. Note these are the dangerous image codecs elsewhere (e.g. Poppler's JPX CVE-2026-12600); the dimension guard below matters if any is ever enabled.

Prior art we can borrow from

PdfPig is Apache-2.0, so these are all licence-compatible to copy code or constants from:

Project Approach Bound Licence
pypdf incremental cap, all filters 75 MB BSD-3
PDFium hard output cap + safe growth 1 GiB BSD-3
Apache Tika output-to-input ratio guard 100:1 Apache-2.0
pdfcpu wrap decoders in io.LimitedReader configurable Apache-2.0
QPDF incremental byte counter opt-in Apache-2.0

Two lessons from their advisories:

  • Bound while decoding, not after. pypdf rejected a Brotli PR that checked size after a full decompress, because the OOM already happened.
  • Cover every filter. pypdf had to ship an LZW follow-up because LZW still allowed 1 GB after Flate was capped.
  • Never trust a declared size. /DL, /Length1, /Width·/Height are attacker-controlled.

Proposed fix (least invasive first)

  1. Per-decode cap. Add an output ceiling to the decode loop of the four ratio bombs (Flate, LZW, Brotli, RunLength); throw when exceeded. A constant to start, no API change.
  2. Guard declared dimensions. In CcittFaxDecodeFilter (and any future image codec), reject cols * rows past a sane maximum before allocating, and never trust the declared size past the theoretical ratio.
  3. Make it an option. Surface MaxDecompressedStreamBytes (and optionally a document-wide budget) on ParsingOptions, threaded into the filter chain so nested stages are covered.

PdfPig is lenient by default, so I'd lean toward a generous but finite per-stream ceiling always on, plus an opt-in aggregate budget. Happy to send a PR once the shape and default are agreed.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions