Skip to content

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

Open
brendancol wants to merge 1 commit intomainfrom
issue-1536
Open

Cap per-tile compressed byte_count in HTTP COG reader (fixes #1536)#1538
brendancol wants to merge 1 commit intomainfrom
issue-1536

Conversation

@brendancol
Copy link
Copy Markdown
Contributor

Summary

  • Add MAX_TILE_BYTES_DEFAULT = 256 << 20 per-tile compressed-byte cap in _reader.py, overridable via XRSPATIAL_COG_MAX_TILE_BYTES.
  • Reject any tile in _fetch_decode_cog_http_tiles whose byte_count exceeds the cap, with a ValueError that names the offending value.
  • New tests in TestHTTPTileByteCountCap cover the reject path, the env override, and confirm local reads stay unaffected.

Fixes #1536. Found during the May 2026 security sweep on geotiff.

Background

_fetch_decode_cog_http_tiles built fetch_ranges directly from byte_counts[tile_idx] with no validation. With the range coalescer from #1534 in front of it, a crafted TIFF (or a malicious server) could drive the client into arbitrarily large Range GETs. Locally, a forged COG with four tiles each claiming 100 MB drove the client into one 100 MB Range request; the math scales linearly with the attacker's claimed value.

Local-mmap reads are bounded by file size (slicing past EOF silently truncates), so the cap is HTTP-only.

Test plan

  • pytest xrspatial/geotiff/tests/test_security.py -v -- 30 passed (5 new, 25 existing)
  • pytest xrspatial/geotiff/tests/ -q (skipping unrelated matplotlib palette failure in test_features.py) -- 823 passed, 2 skipped
  • Default cap accommodates real COGs (test_normal_cog_still_reads)
  • XRSPATIAL_COG_MAX_TILE_BYTES lifts the cap (test_env_override_lifts_cap)
  • Local reads ignore the HTTP cap (test_local_path_unaffected_by_cap -- cap of 1 byte, local read succeeds)

A crafted TIFF served over HTTP can declare any TileByteCount it
wants. _fetch_decode_cog_http_tiles passed those values straight
into read_ranges_coalesced, so a single Range GET ended up sized
by the attacker's byte counts. A forged COG with four tiles each
claiming 100 MB drove the client into one 100 MB Range request.

Bound per-tile compressed bytes against MAX_TILE_BYTES_DEFAULT
(256 MiB) before building fetch_ranges. Override via
XRSPATIAL_COG_MAX_TILE_BYTES. The local-mmap path is bounded by
file size, so the cap stays HTTP-only.

New tests in test_security.py::TestHTTPTileByteCountCap cover the
reject path, the env override, and the local-read no-op.
@github-actions github-actions Bot added the performance PR touches performance-sensitive code label May 10, 2026
@brendancol brendancol requested a review from Copilot May 10, 2026 02:15
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR mitigates a network DoS vector in the HTTP Cloud-Optimized GeoTIFF (COG) read path by enforcing a configurable per-tile cap on compressed TileByteCounts before issuing HTTP Range requests.

Changes:

  • Added a default 256 MiB per-tile compressed byte cap for HTTP COG reads, overridable via XRSPATIAL_COG_MAX_TILE_BYTES, and enforced during _fetch_decode_cog_http_tiles.
  • Raised a clear ValueError when a tile’s declared TileByteCount exceeds the configured cap.
  • Added regression tests that forge TileByteCounts and validate rejection behavior, env override behavior, and that local reads are unaffected.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
xrspatial/geotiff/_reader.py Introduces the HTTP per-tile compressed-byte cap and enforces it while building fetch ranges.
xrspatial/geotiff/tests/test_security.py Adds regression tests using a mocked HTTP source and a helper to patch TileByteCounts.
.claude/sweep-security-state.csv Updates the security sweep tracking entry for the geotiff module to reflect this fix.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

# only exercise the HTTP path through a mock _HTTPSource.
# ---------------------------------------------------------------------------

import threading
Comment on lines +1343 to +1348
# Each tile's compressed size is bounded against MAX_TILE_BYTES BEFORE
# the fetch list is built. A crafted COG can claim arbitrarily large
# TileByteCounts; without this guard the HTTP layer would issue a
# Range request sized by the attacker's value (issue #1536). The cap
# is overridable via XRSPATIAL_COG_MAX_TILE_BYTES; the local-mmap
# path is naturally bounded by file size and does not need this check.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

performance PR touches performance-sensitive code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants