CVE
CVE-2026-12481
CWE
CWE-502
Affected Surface
- PyPI package `keras` in the 3.14.x line, with public source evidence for 3.14.0 and 3.14.1
- Inference services, notebooks, model-conversion jobs, and internal ML tooling that deserialize attacker-controlled Keras `Lambda` configs
- Applications that call `keras.layers.deserialize(config)`, `keras.models.clone_model(model)`, or `keras.layers.Lambda.from_config(config)` on lower-trust input
- Teams that rely on Keras safe mode as the only boundary between untrusted model content and Python execution
CVE-2026-12481 matters because it breaks a boundary Keras users are explicitly told to trust: safe-mode deserialization of Lambda layers. The July 3 disclosure shows that in the vulnerable 3.14.x logic, an unset safe_mode value can collapse into a falsey value rather than a fail-closed default. Once that happens, Keras stops treating a serialized lambda as dangerous and passes attacker-controlled bytecode into the loader path that rebuilds Python function objects.
That is not a “we parsed some weird model metadata” bug. It is a bytecode-materialization bug in a package that sits inside notebooks, model registries, inference services, CI conversion jobs, and internal ML tooling. If your application accepts or clones lower-trust Keras model content, this belongs on the same review list as any other deserialization-driven code execution surface.
Affected package and version boundary
The affected package is:
kerason PyPI
The public advisory text names 3.14.0, but the publicly visible version boundary is a little subtler than that.
| Package | Ecosystem | Highest-confidence affected versions from public artifacts | Highest-confidence fixed version from public artifacts |
|---|---|---|---|
keras | PyPI | 3.14.0, 3.14.1 | 3.15.0 |
Why this boundary is defensible:
- the NVD entry names
keras-team/keras version 3.14.0 - the public
v3.14.1source still contains the vulnerable fallbacksafe_mode = safe_mode or serialization_lib.in_safe_mode() - the public
v3.15.0source replaces that line with a fail-closedeffective_safe_modecalculation - PyPI shows
3.15.0as the latest release line available at publication time
So while the imported advisory metadata in NVD currently carries a generic affected <= latest shape, the source-level evidence points to 3.14.0 and 3.14.1 as the exposed public PyPI releases and 3.15.0 as the first clearly remediating line.
The practical exposure condition is narrower than “any environment with Keras installed,” but it is still common:
- a service or notebook loads Keras model content from users, partners, or shared storage
- code calls
keras.layers.deserialize(config)orkeras.models.clone_model(model)on lower-trust input - tooling or internal automation invokes
Lambda.from_config()directly without wrapping it inSafeModeScope(True) - teams assume the default
safe_modebehavior is enough to block lambda bytecode from becoming executable Python
The vulnerable code path is short and direct
The 3.14.1 source shows the core bug clearly:
@classmethod
def from_config(cls, config, custom_objects=None, safe_mode=None):
safe_mode = safe_mode or serialization_lib.in_safe_mode()
fn_config = config["function"]
if (
isinstance(fn_config, dict)
and "class_name" in fn_config
and fn_config["class_name"] == "__lambda__"
):
cls._raise_for_lambda_deserialization(safe_mode)
inner_config = fn_config["config"]
fn = python_utils.func_load(
inner_config["code"],
defaults=inner_config["defaults"],
closure=inner_config["closure"],
)
config["function"] = fn
There are two security-significant details here:
safe_modeis derived with Python’s truthiness semantics rather than a strictis not Nonetest.- If the guard does not fire, the code immediately reconstructs the serialized lambda through
python_utils.func_load(...).
The NVD wording calls out the exact bad state: safe_mode is None outside a SafeModeScope context, and the implementation treats that unset state as if safety were effectively off. _raise_for_lambda_deserialization() only blocks when it sees a truthy value:
@staticmethod
def _raise_for_lambda_deserialization(safe_mode):
if safe_mode:
raise ValueError(...)
So the dangerous flow is:
safe_mode=None
-> in_safe_mode() returns an unset / falsey value
-> _raise_for_lambda_deserialization() does not raise
-> func_load() rebuilds the attacker-controlled function
That is exactly the sort of “default-deny became falsey-deny” logic slip that turns a guardrail into a bypass.
Why func_load() matters: this is executable bytecode, not harmless config
The follow-on loader path in the same Keras release shows what happens next:
def func_load(code, defaults=None, closure=None, globs=None):
...
raw_code = codecs.decode(code.encode("ascii"), "base64")
code = marshal.loads(raw_code)
if globs is None:
globs = globals()
return python_types.FunctionType(
code, globs, name=code.co_name, argdefs=defaults, closure=closure
)
This is why the bug is meaningful. Once the Lambda guard is bypassed, Keras does not merely preserve opaque data. It:
- base64-decodes serialized code bytes
- unmarshals them with
marshal.loads(...) - wraps the resulting code object in
types.FunctionType(...)
At that point the model payload has crossed from serialized configuration into executable Python behavior.
Keras’s own documentation already signals that Lambda is a dangerous serialization surface:
- the
Lambdadocs warn that layers are saved by serializing Python bytecode and are “potentially unsafe” - the serialization docs say
safe_mode=Trueis intended to disable unsafe lambda deserialization
CVE-2026-12481 matters because the implementation failed to honor that contract in the unset-safe-mode case.
The affected call sites are more common than they look
The advisory does not limit exposure to full load_model() calls. The public description explicitly names:
keras.layers.deserialize(config)keras.models.clone_model(model)- direct
Lambda.from_config(config)usage
That makes the bug relevant to more than public model upload features. Review these patterns closely:
- internal model-conversion pipelines that deserialize layer config objects
- notebook utilities that clone or rewrite models before export
- ML platform components that normalize user-submitted architectures
- test harnesses that load shared model fixtures from less-trusted repositories or artifacts
- background jobs that inspect or transform saved model configs before serving
The highest-risk environments are the ones application security teams often overlook:
- Jupyter servers with cloud credentials
- model build workers that can read training datasets or secrets
- inference control planes that accept uploaded or synchronized model packages
- CI jobs that benchmark, convert, or re-save third-party Keras models automatically
If an attacker can move a crafted lambda config into one of those processes, the trust boundary is no longer “ML metadata.” It is “Python code running inside the same process that already holds your tokens, data paths, and filesystem access.”
What changed in 3.15.0
The public v3.15.0 source shows the important design correction:
effective_safe_mode = (
safe_mode
if safe_mode is not None
else serialization_lib.in_safe_mode()
)
safe_mode = effective_safe_mode is not False
That is a real security fix, not a cosmetic refactor.
The new behavior does two things the old code did not:
- it distinguishes “unset” from “explicitly disabled”
- it fails closed by treating anything other than explicit
Falseas safe mode enabled
The accompanying comment in the source is blunt and useful:
# ... treating an unset scope as safe. This fails closed so that
# deserializing a `Lambda` outside of a `SafeModeScope` ... does not
# silently allow arbitrary code execution.
That is the exact security property missing from the 3.14.x implementation.
Scoping and detection
Start by identifying whether affected Keras releases are present:
python3 -m pip show keras
python3 -m pip freeze | rg '^keras=='
uv pip tree | rg '^keras '
poetry show keras
Then find code paths that may deserialize lower-trust model content:
rg -n 'clone_model\\(|Lambda\\.from_config\\(|layers\\.deserialize\\(' .
rg -n 'load_model\\(|deserialize_keras_object\\(' .
rg -n 'safe_mode=False|enable_unsafe_deserialization\\(' .
The review question that matters most is:
Can a less-trusted party influence the config or model artifact that
eventually reaches Lambda deserialization in this process?
Pay special attention to:
- uploaded
.kerasmodel handling - shared internal model registries
- benchmark or evaluation tooling that ingests community models
- Jupyter notebooks that load models from object storage or Git repositories
- automated model cloning or graph rewriting in CI
If you discover that untrusted model material was processed through these paths on 3.14.x, treat the host as potentially having executed attacker-supplied Python.
Remediation
The clean fix is straightforward:
- Upgrade
kerasto3.15.0or later. - Remove or isolate direct
Lambda.from_config()usage on lower-trust input. - Avoid
safe_mode=Falseand avoid globally callingkeras.config.enable_unsafe_deserialization()unless you fully trust the model source. - Prefer subclassed layers with explicit
get_config()logic over serializedLambdabytecode wherever you control the model design. - Treat third-party model artifacts as executable content, not as inert data files.
If you cannot upgrade immediately, the safest short-term rule is simple:
untrusted model or config
-> do not deserialize Lambda bytecode in-process
That may mean refusing models that contain serialized lambdas, moving model inspection into a hardened sandbox, or requiring signed provenance for accepted artifacts.
Response guidance
If an affected 3.14.x environment loaded lower-trust Keras model content:
- Assume attacker-controlled Python may have executed in the same process.
- Rotate credentials reachable from that host, including cloud tokens, dataset access keys, CI secrets, and repository tokens.
- Review notebook, worker, and service logs for model loads, clone operations, and suspicious config ingestion during the exposure window.
- Re-audit any internal tooling that uses
safe_mode=Falseor globally disables safe deserialization. - Rebuild or reimage high-value model-processing workers if you cannot confidently bound what executed.
The key AppSec lesson from CVE-2026-12481 is that ML model loaders are code loaders whenever they serialize language-native executable objects. In Keras, Lambda already sat on that boundary. The July 2026 disclosure shows how a single falsey fallback in the guard path was enough to turn “unsafe unless explicitly allowed” into “unsafe unless the caller remembered to set the scope.”
From research to remediation
Check whether this pattern exists in your codebase
Turn this research into a remediation workflow. Scan dependencies and package manifests for similar supply-chain risk, then prioritize fixes with reachability context.