Reject ISO downloads smaller than their declared volume extent
This commit is contained in:
+54
-13
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user