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:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user