CVE-2026-1462
Description
A vulnerability in the TFSMLayer class of the keras package, version 3.13.0, allows attacker-controlled TensorFlow SavedModels to be loaded during deserialization of .keras models, even when safe_mode=True. This bypasses the security guarantees of safe_mode and enables arbitrary attacker-controlled code execution during model inference under the victim's privileges. The issue arises due to the unconditional loading of external SavedModels, serialization of attacker-controlled file paths, and the lack of validation in the from_config() method.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
kerasPyPI | < 3.13.2 | 3.13.2 |
Affected products
1Patches
1b6773d3decaeDisallow TFSMLayer deserialization in safe_mode to prevent external SavedModel execution (#22035)
2 files changed · +66 −3
keras/src/export/tfsm_layer.py+34 −0 modified@@ -2,6 +2,7 @@ from keras.src import layers from keras.src.api_export import keras_export from keras.src.export.saved_model import _list_variables_used_by_fns +from keras.src.saving import serialization_lib from keras.src.utils.module_utils import tensorflow as tf @@ -146,3 +147,36 @@ def get_config(self): "call_training_endpoint": self.call_training_endpoint, } return {**base_config, **config} + + @classmethod + def from_config(cls, config, custom_objects=None, safe_mode=None): + """Creates a TFSMLayer from its config. + Args: + config: A Python dictionary, typically the output of `get_config`. + custom_objects: Optional dictionary mapping names to custom objects. + safe_mode: Boolean, whether to disallow loading TFSMLayer. + When `safe_mode=True`, loading is disallowed because TFSMLayer + loads external SavedModels that may contain attacker-controlled + executable graph code. Defaults to `True`. + Returns: + A TFSMLayer instance. + """ + # Follow the same pattern as Lambda layer for safe_mode handling + effective_safe_mode = ( + safe_mode + if safe_mode is not None + else serialization_lib.in_safe_mode() + ) + + if effective_safe_mode is not False: + raise ValueError( + "Requested the deserialization of a `TFSMLayer`, which " + "loads an external SavedModel. This carries a potential risk " + "of arbitrary code execution and thus it is disallowed by " + "default. If you trust the source of the artifact, you can " + "override this error by passing `safe_mode=False` to the " + "loading function, or calling " + "`keras.config.enable_unsafe_deserialization()." + ) + + return cls(**config)
keras/src/export/tfsm_layer_test.py+32 −3 modified@@ -114,19 +114,48 @@ def test_serialization(self): # Test reinstantiation from config config = reloaded_layer.get_config() - rereloaded_layer = tfsm_layer.TFSMLayer.from_config(config) + rereloaded_layer = tfsm_layer.TFSMLayer.from_config( + config, safe_mode=False + ) self.assertAllClose(rereloaded_layer(ref_input), ref_output, atol=1e-7) # Test whole model saving with reloaded layer inside model = models.Sequential([reloaded_layer]) temp_model_filepath = os.path.join(self.get_temp_dir(), "m.keras") model.save(temp_model_filepath, save_format="keras_v3") reloaded_model = saving_lib.load_model( - temp_model_filepath, - custom_objects={"TFSMLayer": tfsm_layer.TFSMLayer}, + temp_model_filepath, safe_mode=False ) self.assertAllClose(reloaded_model(ref_input), ref_output, atol=1e-7) + def test_safe_mode_blocks_model_loading(self): + temp_filepath = os.path.join(self.get_temp_dir(), "exported_model") + + # Create and export a model + model = get_model() + model(tf.random.normal((1, 10))) + saved_model.export_saved_model(model, temp_filepath) + + # Wrap SavedModel in TFSMLayer and save as .keras + reloaded_layer = tfsm_layer.TFSMLayer(temp_filepath) + wrapper_model = models.Sequential([reloaded_layer]) + + model_path = os.path.join(self.get_temp_dir(), "tfsm_model.keras") + wrapper_model.save(model_path) + + # Default safe_mode=True should block loading + with self.assertRaisesRegex( + ValueError, + "arbitrary code execution", + ): + saving_lib.load_model(model_path) + + # Explicit opt-out should allow loading + loaded_model = saving_lib.load_model(model_path, safe_mode=False) + + x = tf.random.normal((2, 10)) + self.assertAllClose(loaded_model(x), wrapper_model(x)) + def test_errors(self): # Test missing call endpoint temp_filepath = os.path.join(self.get_temp_dir(), "exported_model")
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5News mentions
0No linked articles in our index yet.