# numpy `np.load` — crafted `.npz` leaks an uncaught `NotImplementedError` (unsupported ZIP compression method) **Out-of-contract exception / DoS. Distinct from the separate `zipfile.BadZipFile` npz finding.** - **Target:** `numpy` (PyPI) - **Verified versions:** numpy **2.5.1** (venv, Python 3.13.12) and numpy **2.3.5** (system, Python 3.13.12) — identical behavior - **Component:** `numpy/lib/_npyio_impl.py` — `load()` (~line 471) routes ZIP-prefixed files to `NpzFile`; `NpzFile.__getitem__` (line 245) - **Class:** Uncaught / out-of-contract exception on untrusted model-file input → denial of service - **CWE:** CWE-248 (Uncaught Exception) / CWE-755 (Improper Handling of Exceptional Conditions) --- ## Summary `np.load()` on a file that begins with the ZIP prefix (`PK\x03\x04`) is unconditionally routed to `NpzFile`. The `NpzFile` constructor succeeds and returns a normal-looking object whose `.files` correctly lists every member. The archive therefore *passes* any load-time validation a caller performs. The crash fires later, on **member access**. `NpzFile.__getitem__` does: ```python # numpy/lib/_npyio_impl.py (line 245) def __getitem__(self, key): ... if magic == self.zip.open(key).read(len(magic)): # (elided) ... with self.zip.open(key) as bytes: # <-- NO translation of ZIP-layer exceptions ... ``` numpy silently assumes every member uses a compression method the stdlib `zipfile` can read. If a member's compression-method field (in both the local file header and the central-directory record) is set to a value `zipfile` does not support — e.g. `99`, or `6`/imploded, anything outside `STORED / DEFLATED / BZIP2 / LZMA` — then `zipfile.ZipExtFile.__init__` → `_get_decompressor` → `_check_compression` raises: ``` NotImplementedError: That compression method is not supported ``` `NotImplementedError` is a subclass of `RuntimeError`. It is **not** in `np.load`'s documented `Raises` contract (`OSError`, `UnpicklingError`, `ValueError`, `EOFError`) and is **not** a subclass of any of them, so it propagates unchanged out of `np.load(...)[key]`. ## Why this is distinct from the `BadZipFile` npz finding A fix that catches `zipfile.BadZipFile` (as the separate npz report proposes) does **not** cover this bug: - Different exception **type**: `NotImplementedError` vs `BadZipFile`. - Different **failure point**: raised during *decompressor selection* inside `zip.open()`, **before** any read / CRC check — so a `BadZipFile`-only handler never sees it. TEST 3 below hardens the wrapper with `BadZipFile` and the crash **still escapes**, proving the two findings are not the same defect. ## Impact Any library or service that calls `np.load()` on an attacker-supplied `.npz` and later touches a member — the normal usage pattern — is vulnerable to an unhandled-exception DoS that is invisible to load-time archive validation. A closely-related second instance shares the identical missing-translation root cause and is reachable **non-adversarially**: because `np.savez_compressed` writes DEFLATE members, a truncated or corrupt compressed `.npz` makes `f['data']` raise an uncaught `zlib.error` (see SECONDARY below). ## PoC Build a valid single-member `ZIP_STORED` `.npz` containing `data.npy` (a real `uint8` array `[1,2,3]`) with numpy's own `zipfile`, then hex-patch the 2-byte compression-method field to `99` in **both** the local file header (offset `i+8` after `PK\x03\x04`) and the central-directory record (offset `j+10` after `PK\x01\x02`). Result is a **247-byte** `.npz`. ```python import numpy f = numpy.load('poc_unsupported_compression.npz', allow_pickle=False) # SUCCEEDS assert f.files == ['data'] # archive looks valid f['data'] # -> NotImplementedError: That compression method is not supported ``` - **Negative control:** the byte-for-byte identical archive *without* the method patch loads cleanly and returns `[1 2 3]`. - **Distinctness control:** wrapping in `except (OSError, ValueError, EOFError, UnpicklingError, BadZipFile)` **still crashes**. Files in this repo: `verify_final.py` (self-contained harness that regenerates all artifacts), `poc_unsupported_compression.npz`, `poc_corrupt_deflate.npz`, `ctrl_valid.npz`. ## Captured evidence (verbatim) — numpy 2.5.1, Python 3.13.12 ``` === ENV === numpy 2.5.1 python 3.13.12 np.load Raises contract (docstring): OSError, UnpicklingError, ValueError, EOFError === PoC written: .../poc_unsupported_compression.npz (247 bytes) === === TEST 1: np.load returns NpzFile fine (constructor OK) === np.load OK, type: NpzFile files: ['data'] === TEST 2: member access f['data'] -> full traceback === *** OUT-OF-CONTRACT *** builtins.NotImplementedError: That compression method is not supported MRO: ['NotImplementedError', 'RuntimeError', 'Exception', 'BaseException', 'object'] --- verbatim traceback --- === TEST 3: defensive wrapper that ALSO catches BadZipFile still crashes === *** STILL ESCAPES even a BadZipFile-hardened wrapper ***: builtins.NotImplementedError === NEGATIVE CONTROL: identical archive without the compress-method patch loads fine === ctrl f['data'] = [1 2 3] === SECONDARY (related, distinct type): corrupt DEFLATE member -> zlib.error === -> zlib.error: Error -3 while decompressing data: invalid code lengths set | MRO tail: ['Exception', 'BaseException', 'object'] done ``` Full stderr traceback (TEST 2): ``` Traceback (most recent call last): File "verify_final.py", line 34, in arr=f['data'] ~^^^^^^^^ File ".../numpy/lib/_npyio_impl.py", line 245, in __getitem__ with self.zip.open(key) as bytes: ~~~~~~~~~~~~~^^^^^ File "/usr/lib/python3.13/zipfile/__init__.py", line 1724, in open return ZipExtFile(zef_file, mode + 'b', zinfo, pwd, True) File "/usr/lib/python3.13/zipfile/__init__.py", line 905, in __init__ self._decompressor = _get_decompressor(self._compress_type) ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^ File "/usr/lib/python3.13/zipfile/__init__.py", line 801, in _get_decompressor _check_compression(compress_type) ~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^ File "/usr/lib/python3.13/zipfile/__init__.py", line 781, in _check_compression raise NotImplementedError("That compression method is not supported") NotImplementedError: That compression method is not supported ``` Reproduced identically on **system numpy 2.3.5, Python 3.13.12**. ## SECONDARY (same root cause, distinct type, non-adversarial) A member with a corrupt DEFLATE stream makes `f['data']` raise an uncaught `zlib.error: Error -3 while decompressing data: invalid code lengths set`. `zlib.error` is likewise outside `np.load`'s `Raises` contract. This is realistic even without an attacker because `np.savez_compressed` produces DEFLATE members, so a truncated/corrupt compressed `.npz` triggers it. ## Suggested fix In `NpzFile.__getitem__`, translate ZIP-layer decompression failures (`NotImplementedError`, `zlib.error`, `BadZipFile`, `EOFError`) raised by `self.zip.open(key)` / subsequent reads into an in-contract `OSError`/`ValueError` (`BadZipFile` already is an `OSError`), so member access honors the documented `Raises` contract. ## Dedup note - Distinct from the separate `.npz` `BadZipFile` load-contract finding (different exception type and failure point; TEST 3 proves a `BadZipFile`-only fix leaves this open). - Distinct from the `.npy` header findings (bool/shape TypeError, descr-tuple IndexError, shape int overflow) — those are `.npy` header-parse bugs, not the `.npz`/ZIP decompression path. - No known CVE covers an uncaught `NotImplementedError`/`zlib.error` from `NpzFile` member access.