refactor(imageinput): consolidate decompression-bomb guard (#5328)

Move the decompression bomb detection in the TIFF reader into into
ImageInput::check_compression_ratio() so it can be used uniformly for
other input types as well.

Assisted-by: Claude Code / Claude Sonnet 5

Signed-off-by: Larry Gritz <lg@larrygritz.com>
This commit is contained in:
Larry Gritz
2026-07-19 21:43:06 -07:00
committed by GitHub
parent 51f99b8f14
commit d9e0610989
3 changed files with 85 additions and 19 deletions
+50
View File
@@ -2311,6 +2311,56 @@ protected:
// Reserved for future use
};
/// Helper: guard against "decompression bomb" headers -- files that are
/// tiny on disk but declare an implausibly large uncompressed pixel data
/// size, whether due to corruption or malicious crafting. This is a
/// narrower, format-specific companion to check_open()'s absolute
/// `"limits:imagesize_MB"` check: a bogus resolution can easily land
/// under that absolute limit while still being wildly disproportionate to
/// the bytes actually available to produce it, and attempting to allocate
/// for or decode such a claim can hang or exhaust memory before any other
/// check has a chance to fire. Real compressed image data for any current
/// format does not approach these ratios, so genuine files are
/// unaffected.
///
/// @param declared_bytes
/// The uncompressed pixel data size implied by the file's header
/// (for example, `spec.image_bytes(true)`).
///
/// @param filesize
/// The size in bytes of the input file or stream. If this is not
/// known (i.e. is 0), no check is performed (this method returns
/// `true`).
///
/// @param max_ratio
/// The largest declared:file size ratio considered plausible for
/// this format. Callers should pick the smallest value that doesn't
/// produce false positives for genuine files; 10000 is a reasonable
/// default shared by most formats.
///
/// @param min_declared_bytes
/// Claims below this absolute size (default 1 GB) are never rejected,
/// regardless of ratio -- small files are cheap to decode even if
/// the ratio looks unusual, so there's no need to risk a false
/// positive.
///
/// @returns
/// Return `true` if the claimed size is plausible (or can't be
/// checked), otherwise return `false` and make an appropriate call
/// to `this->errorfmt()` to record the error.
///
bool check_compression_ratio(imagesize_t declared_bytes,
imagesize_t filesize,
imagesize_t max_ratio = 10000,
imagesize_t min_declared_bytes = (1ULL << 30));
/// Convenience overload of check_compression_ratio() that computes the
/// declared uncompressed size from an ImageSpec (via
/// `spec.image_bytes(true)`).
bool check_compression_ratio(const ImageSpec& spec, imagesize_t filesize,
imagesize_t max_ratio = 10000,
imagesize_t min_declared_bytes = (1ULL << 30));
/// Helper method: Validate that a `[chbegin, chend)` X `[xbegin, xend)` X
/// `[ybegin, yend)` X `[zbegin, zend)` slab of native channel data types
/// (according to the ImageSpec) can fit into the amount of contiguous
+29
View File
@@ -1662,6 +1662,35 @@ ImageInput::check_open(const ImageSpec& spec, ROI range, uint64_t /*flags*/)
bool
ImageInput::check_compression_ratio(imagesize_t declared_bytes,
imagesize_t filesize, imagesize_t max_ratio,
imagesize_t min_declared_bytes)
{
if (filesize == 0 || declared_bytes < min_declared_bytes)
return true;
if (declared_bytes > filesize * max_ratio) {
errorfmt(
"{} header claims a {} MB image from a {} byte file; probably a corrupt or malicious header",
format_name(), declared_bytes >> 20, filesize);
return false;
}
return true;
}
bool
ImageInput::check_compression_ratio(const ImageSpec& spec, imagesize_t filesize,
imagesize_t max_ratio,
imagesize_t min_declared_bytes)
{
return check_compression_ratio(spec.image_bytes(true), filesize, max_ratio,
min_declared_bytes);
}
bool
ImageInput::valid_raw_span_size(cspan<std::byte> buf, const ImageSpec& spec,
int xbegin, int xend, int ybegin, int yend,
+6 -19
View File
@@ -1024,27 +1024,14 @@ TIFFInput::seek_subimage(int subimage, int miplevel)
{ 0, 1 << 30, 0, 1 << 30, 0, 1 << 16, 0, 1 << 16 }))
return false;
// Guard against decompression bombs / corrupt headers. check_open's
// absolute size limit (limits:imagesize_MB) does not catch a bogus
// resolution that happens to land under the limit -- a tiny file
// Guard against decompression bombs / corrupt headers: a tiny file
// claiming a multi-gigabyte image, which would then make the strip/
// scanline readers below allocate and attempt to fill gigabytes of
// pixel data. A real TIFF that decodes to a large image is itself
// large (even with compression), so this does not reject genuine
// files; it only trips on tiny files claiming enormous resolutions.
{
const imagesize_t bomb_ratio = 10000;
imagesize_t uncompressed = m_spec.image_bytes(true);
int64_t filesize = ioproxy() ? ioproxy()->size()
: Filesystem::file_size(m_filename);
if (uncompressed > (imagesize_t(1) << 30) && filesize > 0
&& uncompressed > imagesize_t(filesize) * bomb_ratio) {
errorfmt("TIFF header claims a {} MB image from a {} byte file; "
"probably a corrupt or malicious header",
uncompressed >> 20, filesize);
return false;
}
}
// pixel data.
imagesize_t filesize = ioproxy() ? ioproxy()->size()
: Filesystem::file_size(m_filename);
if (!check_compression_ratio(m_spec, filesize))
return false;
m_subimage = orig_subimage;
m_miplevel = miplevel;
return true;