Restrict unpickling when loading IMDB and Reuters datasets - #23047
Conversation
`keras.datasets.imdb.load_data` and `keras.datasets.reuters.load_data` loaded their `.npz` files with `np.load(allow_pickle=True)`. These two datasets store ragged sequences as `dtype=object` arrays, so pickling is required to read them and `allow_pickle=False` (as applied to the numeric datasets in keras-team#23026 and keras-team#23034) cannot be used here. Add `keras.src.datasets.npz_utils.load_npz`, which reads each archive member with pickling disabled for numeric arrays and, for object arrays, uses a restricted unpickler that only permits numpy array reconstruction. This keeps the existing dataset format working while preventing a crafted `.npz` from executing arbitrary code through a pickle `__reduce__` gadget.
There was a problem hiding this comment.
Code Review
This pull request introduces a secure utility npz_utils to safely load .npz files containing ragged object arrays (used in IMDB and Reuters datasets) without allowing arbitrary code execution via pickling. It replaces the unsafe np.load(allow_pickle=True) calls with a restricted unpickler that only permits NumPy array reconstruction. The review feedback suggests extending the .npy header parsing in _load_npy_member to support version 3.0 headers (using read_array_header_3_0) to ensure compatibility with modern NumPy files.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #23047 +/- ##
==========================================
+ Coverage 84.74% 84.82% +0.07%
==========================================
Files 464 465 +1
Lines 68883 69041 +158
Branches 11315 11346 +31
==========================================
+ Hits 58378 58562 +184
+ Misses 7562 7546 -16
+ Partials 2943 2933 -10
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
`_load_npy_member` only advanced past version 1.0 and 2.0 `.npy` headers before handing the stream to the restricted unpickler, and raised `ValueError` for anything else. numpy also supports version 3.0, so handle it too. numpy exposes no public `read_array_header_3_0`; the 3.0 format only differs from 2.0 by encoding the header string as UTF-8 instead of latin1, while the 4-byte header-length layout we skip past is identical, so 2.0's header reader handles both.
- Drop the leading underscore from `RestrictedUnpickler` and `load_npy_member`, matching the keras/src convention for module-level helpers. - Trim `_ALLOWED_PICKLE_GLOBALS` to the globals IMDB and Reuters actually use. `np.load(allow_pickle=False)` refuses object arrays entirely, and rebuilding the stored `dtype=object` arrays only needs `numpy.ndarray`, `numpy.dtype` and `multiarray._reconstruct` (both the numpy < 2 and numpy >= 2 spellings). The `multiarray.scalar` globals were never exercised by either dataset, verified against the real IMDB and Reuters `.npz` files, so they are removed.
|
Thanks for the review @hertschuh! Addressed in 290b22e.
Just for my understanding, what in this list is used by Reuters and IMDB that is not allowed by np.load(allow_pickle=False)? Good question. The short answer is that none of them are allowed by allow_pickle=False. That flag refuses object arrays entirely (ValueError: Object arrays cannot be loaded when allow_pickle=False), so it can't load these datasets at all. IMDB and Reuters store their ragged sequences as dtype=object arrays, which numpy can only persist as a pickle stream. I traced find_class against the actual imdb.npz / reuters.npz, and each object member only ever needs three globals:
The hosted files were written with numpy < 2, hence the numpy.core spelling; a numpy ≥ 2 re-save emits numpy._core.multiarray._reconstruct instead, which is why both spellings are listed. multiarray.scalar was never actually exercised by either dataset (the elements are plain Python lists of ints, not numpy scalars), so I've dropped those two entries — the allowlist is now exactly the set the datasets need, and anything else (os.system, builtins.eval, …) still raises UnpicklingError. |
hertschuh
left a comment
There was a problem hiding this comment.
Thanks for implementing this!
* Restrict unpickling when loading IMDB and Reuters datasets (#23047) * Restrict unpickling when loading IMDB and Reuters datasets `keras.datasets.imdb.load_data` and `keras.datasets.reuters.load_data` loaded their `.npz` files with `np.load(allow_pickle=True)`. These two datasets store ragged sequences as `dtype=object` arrays, so pickling is required to read them and `allow_pickle=False` (as applied to the numeric datasets in #23026 and #23034) cannot be used here. Add `keras.src.datasets.npz_utils.load_npz`, which reads each archive member with pickling disabled for numeric arrays and, for object arrays, uses a restricted unpickler that only permits numpy array reconstruction. This keeps the existing dataset format working while preventing a crafted `.npz` from executing arbitrary code through a pickle `__reduce__` gadget. * Support npy format version 3.0 in restricted npz loader `_load_npy_member` only advanced past version 1.0 and 2.0 `.npy` headers before handing the stream to the restricted unpickler, and raised `ValueError` for anything else. numpy also supports version 3.0, so handle it too. numpy exposes no public `read_array_header_3_0`; the 3.0 format only differs from 2.0 by encoding the header string as UTF-8 instead of latin1, while the 4-byte header-length layout we skip past is identical, so 2.0's header reader handles both. * Apply review feedback to the restricted npz loader - Drop the leading underscore from `RestrictedUnpickler` and `load_npy_member`, matching the keras/src convention for module-level helpers. - Trim `_ALLOWED_PICKLE_GLOBALS` to the globals IMDB and Reuters actually use. `np.load(allow_pickle=False)` refuses object arrays entirely, and rebuilding the stored `dtype=object` arrays only needs `numpy.ndarray`, `numpy.dtype` and `multiarray._reconstruct` (both the numpy < 2 and numpy >= 2 spellings). The `multiarray.scalar` globals were never exercised by either dataset, verified against the real IMDB and Reuters `.npz` files, so they are removed. * Verify all intermediary H5 groups when navigating H5 files. (#23168) The H5 library is able to resolve nested group addressed using slashes in the name. Instead, resolve these manually to verify the group type each step of the way. * [Fix] Refactor _load_state to capture weight store while preserving Keras test passing (#23226) * Fixes for Python 3.14. (#23259) Python 3.14 no longer allows `NotImplementedError`s to be treated as booleans, instead it actually throws. We were using things like `(-1).__ne__` to filter out dimensions, but this didn't handle `None` dimensions correctly. Replaced with list comprehension. * Reject decompression-bomb members on the .keras asset extraction path (#23101) * Reject decompression-bomb archive members on the .keras asset extract path The decompression-bomb guard `_reject_zip_bomb` is only applied to the in-memory reads (config.json, model.weights.h5, the shard map). When a `.keras` archive has more than three members, the loader builds a disk-backed asset store whose constructor extracts the whole archive with `ZipFile.extractall`, decompressing every member to disk with no ratio check. An attacker-added member that decompresses far beyond its stored size therefore lands on disk in full, exhausting it from a tiny file under the default safe mode. Add `_reject_zip_extract_bomb`, which applies the same per-member declared-vs-stored ratio to every member before extraction. The per-member ratio is used (not the aggregate) so a bomb member cannot be diluted by genuine weights stored alongside it; genuine `.keras` members are stored uncompressed, so the ratio is false-positive-free. * Address review: trim comments and drop in-test keras import - Shorten the extract-bomb floor comment to a single line - Remove the self-explanatory call-site and test-method comments - Use the module-level keras import in the test instead of a local one * Restrict unpickling when loading CIFAR datasets (#23252) * restrict unpickling when loading CIFAR datasets read data_batch_*/test_batch through the numpy-only RestrictedUnpickler so a tampered batch file cannot run a __reduce__ gadget * Use platform-independent gadget in cifar unpickling test * Drop cifar unpickling test and inline comment * Fix TraceContext error for NNX backend reported in #23289 (#23326) * fix trace context error bug in keras * cleanup * fix * fix * clean up PR * code reformat * Bump version to 3.15.1 --------- Co-authored-by: Ziyu Lin <104151270+LinZiyuu@users.noreply.github.com> Co-authored-by: hertschuh <1091026+hertschuh@users.noreply.github.com> Co-authored-by: Suhana <suhanaaa@google.com> Co-authored-by: SABITHSAHEB <shabi7204192361@gmail.com> Co-authored-by: Divyashree Sreepathihalli <divyashreepathihalli@gmail.com>
* Restrict unpickling when loading IMDB and Reuters datasets (#23047) * Restrict unpickling when loading IMDB and Reuters datasets `keras.datasets.imdb.load_data` and `keras.datasets.reuters.load_data` loaded their `.npz` files with `np.load(allow_pickle=True)`. These two datasets store ragged sequences as `dtype=object` arrays, so pickling is required to read them and `allow_pickle=False` (as applied to the numeric datasets in #23026 and #23034) cannot be used here. Add `keras.src.datasets.npz_utils.load_npz`, which reads each archive member with pickling disabled for numeric arrays and, for object arrays, uses a restricted unpickler that only permits numpy array reconstruction. This keeps the existing dataset format working while preventing a crafted `.npz` from executing arbitrary code through a pickle `__reduce__` gadget. * Support npy format version 3.0 in restricted npz loader `_load_npy_member` only advanced past version 1.0 and 2.0 `.npy` headers before handing the stream to the restricted unpickler, and raised `ValueError` for anything else. numpy also supports version 3.0, so handle it too. numpy exposes no public `read_array_header_3_0`; the 3.0 format only differs from 2.0 by encoding the header string as UTF-8 instead of latin1, while the 4-byte header-length layout we skip past is identical, so 2.0's header reader handles both. * Apply review feedback to the restricted npz loader - Drop the leading underscore from `RestrictedUnpickler` and `load_npy_member`, matching the keras/src convention for module-level helpers. - Trim `_ALLOWED_PICKLE_GLOBALS` to the globals IMDB and Reuters actually use. `np.load(allow_pickle=False)` refuses object arrays entirely, and rebuilding the stored `dtype=object` arrays only needs `numpy.ndarray`, `numpy.dtype` and `multiarray._reconstruct` (both the numpy < 2 and numpy >= 2 spellings). The `multiarray.scalar` globals were never exercised by either dataset, verified against the real IMDB and Reuters `.npz` files, so they are removed. * Verify all intermediary H5 groups when navigating H5 files. (#23168) The H5 library is able to resolve nested group addressed using slashes in the name. Instead, resolve these manually to verify the group type each step of the way. * Reject decompression-bomb members on the .keras asset extraction path (#23101) * Reject decompression-bomb archive members on the .keras asset extract path The decompression-bomb guard `_reject_zip_bomb` is only applied to the in-memory reads (config.json, model.weights.h5, the shard map). When a `.keras` archive has more than three members, the loader builds a disk-backed asset store whose constructor extracts the whole archive with `ZipFile.extractall`, decompressing every member to disk with no ratio check. An attacker-added member that decompresses far beyond its stored size therefore lands on disk in full, exhausting it from a tiny file under the default safe mode. Add `_reject_zip_extract_bomb`, which applies the same per-member declared-vs-stored ratio to every member before extraction. The per-member ratio is used (not the aggregate) so a bomb member cannot be diluted by genuine weights stored alongside it; genuine `.keras` members are stored uncompressed, so the ratio is false-positive-free. * Address review: trim comments and drop in-test keras import - Shorten the extract-bomb floor comment to a single line - Remove the self-explanatory call-site and test-method comments - Use the module-level keras import in the test instead of a local one * Restrict unpickling when loading CIFAR datasets (#23252) * restrict unpickling when loading CIFAR datasets read data_batch_*/test_batch through the numpy-only RestrictedUnpickler so a tampered batch file cannot run a __reduce__ gadget * Use platform-independent gadget in cifar unpickling test * Drop cifar unpickling test and inline comment * Bump version to 3.12.4 --------- Co-authored-by: Ziyu Lin <104151270+LinZiyuu@users.noreply.github.com> Co-authored-by: hertschuh <1091026+hertschuh@users.noreply.github.com> Co-authored-by: SABITHSAHEB <shabi7204192361@gmail.com>
…m#23047) * Restrict unpickling when loading IMDB and Reuters datasets `keras.datasets.imdb.load_data` and `keras.datasets.reuters.load_data` loaded their `.npz` files with `np.load(allow_pickle=True)`. These two datasets store ragged sequences as `dtype=object` arrays, so pickling is required to read them and `allow_pickle=False` (as applied to the numeric datasets in keras-team#23026 and keras-team#23034) cannot be used here. Add `keras.src.datasets.npz_utils.load_npz`, which reads each archive member with pickling disabled for numeric arrays and, for object arrays, uses a restricted unpickler that only permits numpy array reconstruction. This keeps the existing dataset format working while preventing a crafted `.npz` from executing arbitrary code through a pickle `__reduce__` gadget. * Support npy format version 3.0 in restricted npz loader `_load_npy_member` only advanced past version 1.0 and 2.0 `.npy` headers before handing the stream to the restricted unpickler, and raised `ValueError` for anything else. numpy also supports version 3.0, so handle it too. numpy exposes no public `read_array_header_3_0`; the 3.0 format only differs from 2.0 by encoding the header string as UTF-8 instead of latin1, while the 4-byte header-length layout we skip past is identical, so 2.0's header reader handles both. * Apply review feedback to the restricted npz loader - Drop the leading underscore from `RestrictedUnpickler` and `load_npy_member`, matching the keras/src convention for module-level helpers. - Trim `_ALLOWED_PICKLE_GLOBALS` to the globals IMDB and Reuters actually use. `np.load(allow_pickle=False)` refuses object arrays entirely, and rebuilding the stored `dtype=object` arrays only needs `numpy.ndarray`, `numpy.dtype` and `multiarray._reconstruct` (both the numpy < 2 and numpy >= 2 spellings). The `multiarray.scalar` globals were never exercised by either dataset, verified against the real IMDB and Reuters `.npz` files, so they are removed.
Description
keras.datasets.imdb.load_dataandkeras.datasets.reuters.load_dataloadtheir
.npzfiles withnp.load(allow_pickle=True). Loading a crafted.npztherefore unpickles arbitrary objects, which can execute code through a pickle
__reduce__gadget (insecure deserialization, CWE-502).#23026 and #23034 removed
allow_pickle=Truefrom the numeric datasets(
mnist,boston_housing,california_housing) andNpzIOStore, but IMDB andReuters were left unchanged. These two datasets store ragged sequences as
dtype=objectarrays, which numpy can only persist via pickle, soallow_pickle=Falsecannot be used here without breaking the dataset format.This PR adds
keras.src.datasets.npz_utils.load_npz, used by both loaders:array reconstruction (
numpy.ndarray,numpy.dtype, and thenumpy(._core).multiarray_reconstruct/scalarglobals).This keeps the existing dataset format working while preventing a crafted
.npzfrom executing arbitrary code. A newnpz_utils_test.pycovers loadingragged object arrays, loading numeric arrays, and rejecting a pickle gadget
without executing its payload.
Contributor Agreement