Reject ISO downloads smaller than their declared volume extent

This commit is contained in:
Codex
2026-10-13 15:24:00 +03:00
parent 8c8ece99f1
commit 4e65a0bdc3
2 changed files with 136 additions and 19 deletions
+54 -13
View File
@@ -47,25 +47,66 @@ class _SafeRedirect(urllib.request.HTTPRedirectHandler):
return super().redirect_request(req, fp, code, msg, headers, newurl)
def _both_endian(field: bytes) -> int:
half = len(field) // 2
little = int.from_bytes(field[:half], 'little')
if little != int.from_bytes(field[half:], 'big'):
raise MediaError('Invalid ISO9660 descriptor: inconsistent byte-order fields')
return little
def _volume_extent(stream) -> None:
"""Check ECMA-119 6.7.1 and PVD fields 8.4.8/8.4.12 against the file."""
size = os.fstat(stream.fileno()).st_size
extent = None
udf = False
terminated = False
stream.seek(16 * 2048)
for index in range(16, 257):
sector = stream.read(2048)
if len(sector) != 2048:
break
if sector[1:6] in (b'NSR02', b'NSR03') and sector[6] == 1:
udf = True
if sector[1:6] != b'CD001':
continue
if sector[0] == 1:
if sector[6] != 1:
raise MediaError('Unsupported ISO9660 primary volume descriptor version')
blocks = _both_endian(sector[80:88])
block_size = _both_endian(sector[128:132])
# Raw ISO files use 2048-byte sectors. ECMA-119 6.2.2 permits
# power-of-two logical blocks from 512 bytes up to sector size.
if not blocks or block_size not in (512, 1024, 2048):
raise MediaError('Invalid ISO9660 volume extent or logical block size')
declared = blocks * block_size
if declared > size:
raise MediaError(f'ISO is truncated: declared volume extent is {declared} bytes, '
f'but the image contains {size}; retry the direct ISO URL')
if extent is not None and extent != declared:
raise MediaError('Invalid ISO9660 descriptors: inconsistent volume extents')
extent = declared
elif sector[0] == 255 and sector[6] == 1:
terminated = True
if extent is not None and extent < (index + 1) * 2048:
raise MediaError('Invalid ISO9660 volume extent: descriptor set lies outside the volume')
break
if extent is None:
if udf:
raise MediaError('Pure UDF image completeness is unsupported; use an ISO9660 or ISO9660/UDF hybrid ISO')
raise MediaError('Not a supported ISO9660 image; use a direct ISO download URL rather than an HTML download page')
if not terminated:
raise MediaError('ISO9660 descriptor set is incomplete or unsupported; retry a complete ISO image')
def validate_iso(path: Path, *, sha256: str | None = None) -> Path:
"""Recognize ISO9660 or UDF volume descriptors and optional whole-file hash."""
"""Validate ISO9660 extent (including hybrids) and optional whole-file hash."""
wanted = _checksum(sha256)
path = Path(path)
if not path.is_file():
raise MediaError(f'ISO does not exist or is not a file: {path}')
found = False
with path.open('rb') as stream:
stream.seek(16 * 2048)
for _ in range(241):
sector = stream.read(2048)
if len(sector) != 2048:
break
if ((sector[1:6] == b'CD001' and sector[0] in (0, 1, 2, 3, 255) and sector[6] == 1)
or (sector[1:6] in (b'NSR02', b'NSR03') and sector[6] == 1)):
found = True
break
if not found:
raise MediaError('Not an ISO9660/UDF image; use a direct ISO download URL rather than an HTML download page')
_volume_extent(stream)
if wanted:
stream.seek(0)
digest = hashlib.file_digest(stream, 'sha256').hexdigest()