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)
- 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.
- 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.
- 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.
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
Decodecall to allocate up to ~2 GB, and two things make it worse:/Filter [/FlateDecode /FlateDecode]is decoded stage by stage, so the ratios multiply. This triggers on reading the file, before any page is rendered.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 unboundedMemoryStream. Up to ~1032:1 per stage, and a filter chain multiplies it.LzwFilter.cs— unbounded outputMemoryStream; the dictionary also grows without a cap.BrotliFilter.cs— buffer doubles up to0x7FFFFFC7, no cap below the array limit. Highest ratio per byte.RunLengthFilter.cs— unboundedArrayPoolBufferWriter; 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— allocatesnew byte[(cols+7)/8 * rows]from/Columnsand/Rows//Heightin 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:
io.LimitedReaderTwo lessons from their advisories:
/DL,/Length1,/Width·/Heightare attacker-controlled.Proposed fix (least invasive first)
CcittFaxDecodeFilter(and any future image codec), rejectcols * rowspast a sane maximum before allocating, and never trust the declared size past the theoretical ratio.MaxDecompressedStreamBytes(and optionally a document-wide budget) onParsingOptions, 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.