Summary
The TIFF reader checks image dimensions but ignores tile dimensions. An adversarial TIFF can claim a 1x1 image (passes the existing guard) with a 2^30 x 2^30 tile (unchecked), and the decompressor will try to allocate terabytes.
Reproducer
Build a TIFF with:
ImageWidth=1, ImageLength=1 (passes the default ~1B pixel guard)
TileWidth=2^30, TileLength=2^30 (unchecked)
_decode_strip_or_tile in xrspatial/geotiff/_reader.py computes pixel_count = tw * th * samples ≈ 2^60 and passes it to decompress(). For LZW, lzw_decompress then does np.empty(expected_size, dtype=np.uint8) at terabyte scale. OOM.
Affected paths
_read_tiles (_reader.py:436) checks image dims at line 494 but not tw/th.
_read_cog_http (_reader.py:578) has the same gap.
read_geotiff_gpu (__init__.py:1005) checks image dims at line 1109, then gpu_decode_tiles does cupy.zeros(n_tiles * tile_width * tile_height * bytes_per_pixel) with no cap.
The strip path happens to be safe because strip_rows = min(rps, height - strip_row) pins it to the (checked) image height.
Threat model
TIFF from an untrusted source (HTTP, user upload, third-party mosaic) triggers unbounded allocation. No code execution, just DoS -- but a one-file DoS.
Proposed fix
After reading tile dims from the IFD, call _check_dimensions(tile_width, tile_height, samples, max_pixels) in _read_tiles, _read_cog_http, and read_geotiff_gpu. Legitimate COGs use tile_width <= 512, so capping per-tile pixels at max_pixels rejects the attack without breaking anything real.
Tests
- A TIFF with small image dims + huge tile dims raises
ValueError.
- Normal tile sizes (256, 512) still pass.
Found during a security audit of the geotiff subpackage.
Summary
The TIFF reader checks image dimensions but ignores tile dimensions. An adversarial TIFF can claim a 1x1 image (passes the existing guard) with a 2^30 x 2^30 tile (unchecked), and the decompressor will try to allocate terabytes.
Reproducer
Build a TIFF with:
ImageWidth=1, ImageLength=1(passes the default ~1B pixel guard)TileWidth=2^30, TileLength=2^30(unchecked)_decode_strip_or_tileinxrspatial/geotiff/_reader.pycomputespixel_count = tw * th * samples ≈ 2^60and passes it todecompress(). For LZW,lzw_decompressthen doesnp.empty(expected_size, dtype=np.uint8)at terabyte scale. OOM.Affected paths
_read_tiles(_reader.py:436) checks image dims at line 494 but nottw/th._read_cog_http(_reader.py:578) has the same gap.read_geotiff_gpu(__init__.py:1005) checks image dims at line 1109, thengpu_decode_tilesdoescupy.zeros(n_tiles * tile_width * tile_height * bytes_per_pixel)with no cap.The strip path happens to be safe because
strip_rows = min(rps, height - strip_row)pins it to the (checked) image height.Threat model
TIFF from an untrusted source (HTTP, user upload, third-party mosaic) triggers unbounded allocation. No code execution, just DoS -- but a one-file DoS.
Proposed fix
After reading tile dims from the IFD, call
_check_dimensions(tile_width, tile_height, samples, max_pixels)in_read_tiles,_read_cog_http, andread_geotiff_gpu. Legitimate COGs usetile_width <= 512, so capping per-tile pixels atmax_pixelsrejects the attack without breaking anything real.Tests
ValueError.Found during a security audit of the geotiff subpackage.