Commit 2ebb06f2 for libheif
commit 2ebb06f2b60f4542e193ae273988b728ba7c9083
Author: Dirk Farin <dirk.farin@gmail.com>
Date: Sat Sep 5 00:40:59 2026 +0200
Enforce max_items security limit on iinf child boxes (GHSA-vg7w-rp49-4fc2)
Box_iinf::parse passed the file-declared item_count to read_children as a
concrete child count, but the cardinality check in read_children only ran
in the READ_CHILDREN_ALL branch. The limit was therefore never enforced
for iinf, so a crafted file could declare an unbounded number of items,
costing quadratic parsing time (via the per-item iref reference scan) and
large amounts of heap that max_total_memory does not track.
- Box_iinf::parse rejects item_count > max_items up front, mirroring
Box_iloc::parse.
- read_children hoists the child-count limit out of the READ_CHILDREN_ALL
branch and rejects an excessive declared count directly. This also
covers the other concrete-count callers (dref, tili).
- Reject a declared count larger than the remaining input could hold
(minimum box size is 8 bytes), which also bounds the new reserve() when
security limits are disabled.
- Add a post-loop check that the number of children read matches the
declared count, catching clean-boundary short reads that EOF does not
report as an error.
Fixes GHSA-vg7w-rp49-4fc2.
diff --git a/libheif/box.cc b/libheif/box.cc
index d752c7d4..3ea69bf7 100644
--- a/libheif/box.cc
+++ b/libheif/box.cc
@@ -1107,7 +1107,52 @@ bool Box::equal(const std::shared_ptr<Box>& box1, const std::shared_ptr<Box>& bo
Error Box::read_children(BitstreamRange& range, uint32_t max_number, const heif_security_limits* limits)
{
- uint32_t count = 0;
+ // A freshly parsed box has no children yet, so m_children.size() below tracks
+ // the number of children read so far.
+ assert(m_children.empty());
+
+ // Determine the applicable limit on the number of child boxes.
+ uint32_t max_children;
+ if (get_short_type() == fourcc("iinf")) {
+ max_children = limits->max_items;
+ }
+ else {
+ max_children = limits->max_children_per_box;
+ }
+
+ // If the number of children is known in advance, reject an excessive count
+ // directly instead of reading children until the accumulated count reaches
+ // the limit.
+ if (max_number != READ_CHILDREN_ALL) {
+ if (max_children && max_number > max_children) {
+ std::stringstream sstr;
+ sstr << "Number of child boxes (" << max_number << ") in '" << get_type_string()
+ << "' box exceeds the security limit of " << max_children << ".";
+
+ return Error(heif_error_Memory_allocation_error,
+ heif_suberror_Security_limit_exceeded,
+ sstr.str());
+ }
+
+ // The declared number of children cannot exceed what the remaining input
+ // could possibly hold (each box occupies at least an 8-byte header). A
+ // larger count means the box is truncated or malformed, so reject it
+ // before reading anything. This also bounds the reservation below, which
+ // matters when security limits are disabled and 'max_number' is otherwise
+ // unbounded.
+ size_t max_possible_children = range.get_remaining_bytes() / 8;
+ if (max_number > max_possible_children) {
+ std::stringstream sstr;
+ sstr << "'" << get_type_string() << "' box declares " << max_number
+ << " child boxes, but the remaining data can hold at most "
+ << max_possible_children << ".";
+ return Error(heif_error_Invalid_input,
+ heif_suberror_End_of_data,
+ sstr.str());
+ }
+
+ m_children.reserve(max_number);
+ }
while (!range.eof() && !range.error()) {
std::shared_ptr<Box> box;
@@ -1117,14 +1162,6 @@ Error Box::read_children(BitstreamRange& range, uint32_t max_number, const heif_
}
if (max_number == READ_CHILDREN_ALL) {
- uint32_t max_children;
- if (get_short_type() == fourcc("iinf")) {
- max_children = limits->max_items;
- }
- else {
- max_children = limits->max_children_per_box;
- }
-
if (max_children && m_children.size() > max_children) {
std::stringstream sstr;
sstr << "Maximum number of child boxes (" << max_children << ") in '" << get_type_string() << "' box exceeded.";
@@ -1138,17 +1175,26 @@ Error Box::read_children(BitstreamRange& range, uint32_t max_number, const heif_
m_children.push_back(std::move(box));
-
- // count the new child and end reading new children when we reached the expected number
-
- count++;
-
+ // Stop once we have read the expected number of children.
if (max_number != READ_CHILDREN_ALL &&
- count == max_number) {
+ m_children.size() == max_number) {
break;
}
}
+ // If a specific number of children was expected, we must have read exactly
+ // that many. A short read means the box is truncated or malformed. Prefer
+ // this specific error over the lower-level range error below, which would
+ // otherwise mask it.
+ if (max_number != READ_CHILDREN_ALL && m_children.size() != max_number) {
+ std::stringstream sstr;
+ sstr << "'" << get_type_string() << "' box declares " << max_number
+ << " child boxes, but only " << m_children.size() << " could be read.";
+ return Error(heif_error_Invalid_input,
+ heif_suberror_End_of_data,
+ sstr.str());
+ }
+
return range.get_error();
}
@@ -2630,6 +2676,17 @@ Error Box_iinf::parse(BitstreamRange& range, const heif_security_limits* limits)
return Error::Ok;
}
+ // Sanity check.
+ if (limits->max_items && item_count > limits->max_items) {
+ std::stringstream sstr;
+ sstr << "iinf box contains " << item_count << " items, which exceeds the security limit of "
+ << limits->max_items << " items.";
+
+ return Error(heif_error_Memory_allocation_error,
+ heif_suberror_Security_limit_exceeded,
+ sstr.str());
+ }
+
return read_children(range, item_count, limits);
}