# Pillow: Decompression bomb check bypass in SpiderImagePlugin.seek() leads to unbounded allocation DoS

SpiderImagePlugin.seek() re-parses an attacker-controlled header offset and resets image size without re-running the decompression bomb check, so a <1KB SPIDER stack file triggers a ~6.4GB allocation on load().

- Payout: Research
- Severity: high
- Program: Pillow
- Found by: sonicnew
- Disclosed: 2026-10-22
- CVE: CVE-2026-96637
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-g842-9rc4-69pw
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-g842-9rc4-69pw
- Tags: area:oss domain:oss impact:dos kind:research platform:ghsa sev:high tech:python technique:incomplete-fix technique:source-review vuln:dos vuln:memory-corruption

**Target:** Pillow SpiderImagePlugin multi-frame SPIDER stack file parsing
**Root cause:** Image.open() runs _decompression_bomb_check only once in _open_core, but SpiderImageFile.seek() calls self._open() directly at an attacker-influenced stkoffset, which reassigns self._size from unvalidated header floats (nsam/nrow) with no bomb check on the new size.
**Steps:**
1. Set Image.MAX_IMAGE_PIXELS=5000 to make the bomb check observable.
2. CONTROL: Image.open(standalone 100x100 SPIDER) -> DecompressionBombWarning fires correctly.
3. BYPASS: Image.open(stack with frame0=1x1) passes the one-time check.
4. Call im.seek(1): SpiderImageFile.seek computes stkoffset and calls self._open() directly, setting size to (100,100) with no DecompressionBombWarning/Error.
5. Craft frame-1 header with nsam/nrow e.g. 40000x40000; im.load() -> ImageFile.load_prepare() runs Image.core.new(self.mode, self.size) allocating ~6.4 GB (mode F, 4 bytes/pixel) from a file under 1 KB.
**How found:** Compared multi-frame plugins: GifImagePlugin, TiffImagePlugin, IcnsImagePlugin and IcoImagePlugin all re-run Image._decompression_bomb_check on their alternate size-setting paths, while SpiderImagePlugin has no bomb check anywhere. Traced that seek() calls _open() directly rather than through Image.open(), so the one-time check in _open_core is skipped, and that _open() reassigns self._size from unvalidated header floats.
**Exploitation:** Attacker supplies a sub-1KB SPIDER stack file whose frame-1 header declares huge nsam/nrow; victim code that seeks to frame 1 and loads it allocates multiple gigabytes, causing memory exhaustion DoS. No observed in-the-wild use.
**Insight:** For any image/container format with multiple frames or sub-images, enumerate every code path that reassigns the size/dimension fields (seek, tile selection, sub-image open) and check whether the resource-limit validation runs there too; a one-time check in the top-level open() is trivially bypassed by plugins that re-parse headers at attacker-chosen offsets.

