Arbitrary Code Execution via Crafted Keras Config for Model Loading
Description
The Keras Model.load_model function permits arbitrary code execution, even with safe_mode=True, through a manually constructed, malicious .keras archive. By altering the config.json file within the archive, an attacker can specify arbitrary Python modules and functions, along with their arguments, to be loaded and executed during model loading.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Arbitrary code execution in Keras Model.load_model via malicious .keras archive even with safe_mode=True.
The vulnerability in Keras's Model.load_model function allows arbitrary code execution when loading a crafted .keras archive, even with safe_mode=True. The root cause lies in the _retrieve_class_or_fn function used during deserialization, which did not restrict the Python modules that could be imported. By altering config.json within the archive, an attacker can specify arbitrary Python modules and functions to be executed during model loading [1][2].
Exploitation requires only that a user loads a malicious .keras file. No authentication or special network position is needed, making any application that loads Keras models from untrusted sources vulnerable. The attack surface includes machine learning pipelines, model hubs, and any service that accepts user-uploaded models [2].
Successful exploitation gives an attacker full arbitrary code execution in the context of the user or process loading the model. This can lead to data exfiltration, privilege escalation, or complete system compromise [2].
The Keras team has addressed this issue in commit e67ac8f by adding checks to _retrieve_class_or_fn to limit imports to only keras.src modules and by validating deserialized objects [4]. Users should update to the latest Keras version containing this fix. No effective workaround exists other than avoiding loading models from untrusted sources [1][4].
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
kerasPyPI | >= 3.0.0, < 3.9.0 | 3.9.0 |
Affected products
5- osv-coords3 versionspkg:apk/chainguard/kubeflow-pipelines-visualization-serverpkg:apk/wolfi/kubeflow-pipelines-visualization-serverpkg:pypi/keras
< 2.4.1-r2+ 2 more
- (no CPE)range: < 2.4.1-r2
- (no CPE)range: < 2.4.1-r2
- (no CPE)range: >= 3.0.0, < 3.9.0
- Google/Kerasv5Range: 3.0.0
Patches
1e67ac8ffd0c8Add checks to deserialization. (#20751)
2 files changed · +18 −16
keras/src/models/functional.py+6 −0 modified@@ -19,6 +19,7 @@ from keras.src.ops.function import make_node_key from keras.src.ops.node import KerasHistory from keras.src.ops.node import Node +from keras.src.ops.operation import Operation from keras.src.saving import serialization_lib from keras.src.utils import tracking @@ -523,6 +524,11 @@ def process_layer(layer_data): layer = serialization_lib.deserialize_keras_object( layer_data, custom_objects=custom_objects ) + if not isinstance(layer, Operation): + raise ValueError( + "Unexpected object from deserialization, expected a layer or " + f"operation, got a {type(layer)}" + ) created_layers[layer_name] = layer # Gather layer inputs.
keras/src/saving/serialization_lib.py+12 −16 modified@@ -783,22 +783,18 @@ def _retrieve_class_or_fn( # Otherwise, attempt to retrieve the class object given the `module` # and `class_name`. Import the module, find the class. - try: - mod = importlib.import_module(module) - except ModuleNotFoundError: - raise TypeError( - f"Could not deserialize {obj_type} '{name}' because " - f"its parent module {module} cannot be imported. " - f"Full object config: {full_config}" - ) - obj = vars(mod).get(name, None) - - # Special case for keras.metrics.metrics - if obj is None and registered_name is not None: - obj = vars(mod).get(registered_name, None) - - if obj is not None: - return obj + if module == "keras.src" or module.startswith("keras.src."): + try: + mod = importlib.import_module(module) + obj = vars(mod).get(name, None) + if obj is not None: + return obj + except ModuleNotFoundError: + raise TypeError( + f"Could not deserialize {obj_type} '{name}' because " + f"its parent module {module} cannot be imported. " + f"Full object config: {full_config}" + ) raise TypeError( f"Could not locate {obj_type} '{name}'. "
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/advisories/GHSA-48g7-3x6r-xfhpghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-1550ghsaADVISORY
- github.com/keras-team/keras/commit/e67ac8ffd0c883bec68eb65bb52340c7f9d3a903ghsaWEB
- github.com/keras-team/keras/pull/20751ghsaWEB
- github.com/keras-team/keras/releases/tag/v3.9.0ghsaWEB
- github.com/keras-team/keras/security/advisories/GHSA-48g7-3x6r-xfhpghsaWEB
- towerofhanoi.it/writeups/cve-2025-1550/mitre
News mentions
0No linked articles in our index yet.