YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
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 toNpzFile;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:
# 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:
NotImplementedErrorvsBadZipFile. - Different failure point: raised during decompressor selection inside
zip.open(), before any read / CRC check β so aBadZipFile-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.
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 <module>
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
.npzBadZipFileload-contract finding (different exception type and failure point; TEST 3 proves aBadZipFile-only fix leaves this open). - Distinct from the
.npyheader findings (bool/shape TypeError, descr-tuple IndexError, shape int overflow) β those are.npyheader-parse bugs, not the.npz/ZIP decompression path. - No known CVE covers an uncaught
NotImplementedError/zlib.errorfromNpzFilemember access.