Describe the bug
The HTTP COG reader has no cap on per-tile compressed byte sizes. A crafted TIFF (or a malicious HTTP server) can declare any TileByteCounts value it wants, and the reader passes those values straight into read_ranges_coalesced, which then issues a single Range GET sized by the sum of attacker-controlled byte counts.
Where
xrspatial/geotiff/_reader.py:1320-1354: _fetch_decode_cog_http_tiles builds fetch_ranges from raw byte_counts[tile_idx] with no validation.
xrspatial/geotiff/_reader.py:288-358: coalesce_ranges sums adjacent ranges into one merged GET, also unbounded.
xrspatial/geotiff/_reader.py:407-461: _HTTPSource.read_ranges_coalesced issues the merged Range request.
Exploit (verified against a local HTTP server)
I built a tiled COG with a real header, then patched the four TileByteCounts entries to claim 100 MB each. When the client called _read_cog_http(url), it sent one Range request asking for 100 MB. Same trick with one tile claiming 4 GB gets you a 4 GB Range GET.
The decode fails at the size-mismatch check, but the memory is already allocated. If the server trickles the response, that allocation stays open.
Why this slips past existing guards
_check_dimensions(tw, th, samples, max_pixels) at line 1277 caps the decompressed tile size, not the compressed network fetch. The local-mmap path at line 1095 is naturally bounded: mmap slicing past EOF silently truncates. So this only bites on HTTP.
Threat model
Two ways to trigger it:
- Crafted TIFF hosted by the attacker (full control of header values plus server response).
- Honest TIFF hosted by a malicious server that returns oversized Range responses.
xarray-spatial's HTTP reader is meant to be pointed at public COGs from the open web, so the input boundary is wide.
Fix proposal
- Add
MAX_TILE_BYTES_DEFAULT = 256 << 20 (256 MiB) per-tile compressed-size cap in _reader.py, overridable via XRSPATIAL_COG_MAX_TILE_BYTES.
- Reject any tile in
_fetch_decode_cog_http_tiles whose byte_counts[tile_idx] exceeds the cap, with an error that names the offending byte count.
- Add tests under
xrspatial/geotiff/tests/test_security.py:
- Crafted COG with one huge byte_count is rejected.
- Normal COG with realistic tile sizes still reads correctly.
Related PRs
New attack surface from #1534 (HTTP COG range coalescing). The local-file path is unaffected.
Severity
MEDIUM. Network DoS, no RCE. Triggered by crafted file or malicious server.
Describe the bug
The HTTP COG reader has no cap on per-tile compressed byte sizes. A crafted TIFF (or a malicious HTTP server) can declare any
TileByteCountsvalue it wants, and the reader passes those values straight intoread_ranges_coalesced, which then issues a single Range GET sized by the sum of attacker-controlled byte counts.Where
xrspatial/geotiff/_reader.py:1320-1354:_fetch_decode_cog_http_tilesbuildsfetch_rangesfrom rawbyte_counts[tile_idx]with no validation.xrspatial/geotiff/_reader.py:288-358:coalesce_rangessums adjacent ranges into one merged GET, also unbounded.xrspatial/geotiff/_reader.py:407-461:_HTTPSource.read_ranges_coalescedissues the merged Range request.Exploit (verified against a local HTTP server)
I built a tiled COG with a real header, then patched the four
TileByteCountsentries to claim 100 MB each. When the client called_read_cog_http(url), it sent one Range request asking for 100 MB. Same trick with one tile claiming 4 GB gets you a 4 GB Range GET.The decode fails at the size-mismatch check, but the memory is already allocated. If the server trickles the response, that allocation stays open.
Why this slips past existing guards
_check_dimensions(tw, th, samples, max_pixels)at line 1277 caps the decompressed tile size, not the compressed network fetch. The local-mmap path at line 1095 is naturally bounded: mmap slicing past EOF silently truncates. So this only bites on HTTP.Threat model
Two ways to trigger it:
xarray-spatial's HTTP reader is meant to be pointed at public COGs from the open web, so the input boundary is wide.
Fix proposal
MAX_TILE_BYTES_DEFAULT = 256 << 20(256 MiB) per-tile compressed-size cap in_reader.py, overridable viaXRSPATIAL_COG_MAX_TILE_BYTES._fetch_decode_cog_http_tileswhosebyte_counts[tile_idx]exceeds the cap, with an error that names the offending byte count.xrspatial/geotiff/tests/test_security.py:Related PRs
New attack surface from #1534 (HTTP COG range coalescing). The local-file path is unaffected.
Severity
MEDIUM. Network DoS, no RCE. Triggered by crafted file or malicious server.