File size: 3,681 Bytes
c63619c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 | ---
tags:
- security
- vulnerability
- poc
- parquet
- fastparquet
- decompression-bomb
- cwe-789
license: mit
---
# fastparquet β Parquet PageHeader Decompression Bomb (PoC)
**Repo:** `MBM7/fastparquet-parquet-decompression-bomb-poc`
**Status:** Responsible disclosure β submitted to Huntr
**Severity:** High / CWE-789 (Uncontrolled Memory Allocation)
---
## Summary
A crafted **492-byte** `.parquet` file causes `fastparquet` to allocate
**gigabytes of memory** before failing β enabling OOM-kill on any service
that loads user-supplied Parquet files.
| File size | Fake `uncompressed_page_size` | Peak allocation | Amplification |
|-----------|-------------------------------|-----------------|---------------|
| 492 bytes | 500,000,000 | **500 MB** | 1 : 1,016,260 |
| 492 bytes | 2,000,000,000 | **2,000 MB** | 1 : 4,065,040 |
---
## Root Cause
`fastparquet/compression.py` β `decompress_data()`:
```python
if algorithm.upper() in decom_into:
x = np.empty(uncompressed_size, dtype='uint8') # β pre-allocates upfront
decom_into[algorithm.upper()](
np.frombuffer(data, dtype=np.uint8), x
)
return x
```
`uncompressed_size` is passed directly from `ph.uncompressed_page_size`
(Thrift compact i32, PageHeader field 2) **with no bound check** at any
point in the call chain:
```
fastparquet.ParquetFile.to_pandas()
β read_col_group() [core.py ~291]
β _read_page() [core.py ~20]
β decompress_data(
data,
page_header.uncompressed_page_size, β from Thrift, unchecked
codec
)
β np.empty(uncompressed_size) β GB allocation here
```
`max_header_size` or equivalent limit: **does not exist** in fastparquet.
---
## Attack
Parquet format: `PAR1 | PageHeader (Thrift compact) | compressed_data | ... | PAR1`
PageHeader field 2 (`uncompressed_page_size`, type i32) is a
zigzag-encoded varint at byte offset 7 in a minimal single-column file.
Replacing the 1-byte varint (value `4`) with a 5-byte varint
(value `500_000_000`) produces a valid-looking file that triggers 500 MB
of allocation on any codec in `decom_into` (GZIP, SNAPPY, ZSTD, BROTLI).
---
## Reproduce
```bash
pip install fastparquet pyarrow
python poc_fastparquet_parquet_bomb.py
```
Expected output:
```
Crafted file size : 492 bytes
Expected allocation : 500,000,000 bytes
Amplification : 1:1,016,260
Result : DecompressionError: unexpected end of file
Peak memory : 500 MB β allocation happened
```
---
## Suggested Fix
`fastparquet/compression.py`, before `np.empty()`:
```python
MAX_UNCOMPRESSED = 256 * 1024 * 1024 # 256 MB configurable limit
if uncompressed_size > MAX_UNCOMPRESSED:
raise ValueError(
f"uncompressed_size {uncompressed_size} exceeds safety limit "
f"{MAX_UNCOMPRESSED}. Possible decompression bomb."
)
```
---
## Environment
| Package | Version |
|---------------|-----------|
| fastparquet | 2026.5.0 |
| pyarrow | 25.0.0 |
| Python | 3.12 |
---
## Files
| File | Description |
|---------------------------------------|--------------------------|
| `poc_fastparquet_parquet_bomb.py` | PoC script (self-contained) |
| `bomb_500mb.parquet` | Crafted payload (492 bytes) |
---
*Discovered via empirical security research using UBDAF (Universal Bug
Discovery & Analysis Framework) β Q2 CRITICAL pattern: arithmetic on
externally-controlled value without bound check.*
|