Skip to content

Cap per-tile compressed byte_count in HTTP COG reader (DoS) #1536

@brendancol

Description

@brendancol

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

  1. Add MAX_TILE_BYTES_DEFAULT = 256 << 20 (256 MiB) per-tile compressed-size cap in _reader.py, overridable via XRSPATIAL_COG_MAX_TILE_BYTES.
  2. 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.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinginput-validationInput validation and error messages

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions