A picture frame that runs whatever code is scribbled on the back of the photo
This is another entry in the long-running Keras Lambda layer deserialization saga (CWE-502). Version 3.15.0 ships a model-loading path where a crafted .keras archive can smuggle attacker-controlled Python bytecode through Lambda.from_config() because the safe_mode=None default fails open in certain call sites (keras.layers.deserialize, keras.models.clone_model, and any direct from_config invocation outside a SafeModeScope(True) block). When a data scientist or an inference server calls keras.models.load_model('attacker.keras'), arbitrary code executes as the loading process — full RCE at the user/service context.
The vendor's HIGH / 7.8 rating is directionally correct but overstated for a typical enterprise. The CVSS vector (AV:L/AC:L/PR:N/UI:R) already captures the two big shackles: local attack vector and user interaction required. This is not a wormable network bug; it is a supply-chain-flavored code-execution primitive that fires only when someone chooses to load a hostile model file. For a shop that pulls models exclusively from an internal registry with signed provenance, the real severity is MEDIUM. For an ML platform that ingests arbitrary user-submitted .keras files (HuggingFace mirror, Kaggle-style pipeline, MLOps SaaS), the role-multiplier pushes it up to HIGH.
4 steps from start to impact.
Craft a malicious .keras archive
Lambda layer whose serialized function field contains marshalled Python bytecode invoking os.system, subprocess.Popen, or a reverse-shell payload. Public PoCs from Huntr and HiddenLayer already publish gadget templates for exactly this file format.- Attacker can produce a
.keras/.h5file - Knowledge of the Lambda deserialization gadget
- Payload is stored on disk — trivial for AV/YARA to scope-scan if signatures exist
- File carries a
Lambdalayer, which is unusual for production models and can be flagged
sai-security-advisory collection detect marshalled bytecode inside config.json of a .keras archive.Deliver the model to a victim loader
keras.models.load_model() call. Realistic vectors: uploading to a public model hub, submitting a fine-tuned artifact to an MLOps pipeline, phishing a data scientist with a 'pretrained checkpoint,' or compromising an internal artifact registry.- Victim has a workflow that loads externally sourced models
- No signature/provenance check on the artifact
- Mature orgs pull from an internal MLflow/W&B/S3 with hash pinning
- Email gateways strip unknown extensions or quarantine large binaries
.keras downloads from non-allowlisted hosts.Trigger deserialization outside SafeModeScope
load_model(path) — the default safe_mode parameter resolves to None, and the guard in _raise_for_lambda_deserialization compares against False instead of a truthiness check, so the guard is skipped. The marshalled bytecode is unmarshalled and executed inline.- Code path is
load_model/clone_model/layers.deserializewithout explicitsafe_mode=True - Keras 3.15.0 exactly (or the vulnerable range straddling 3.14–3.15)
- Keras 3.10+ prints a loud warning when loading a Lambda layer
- Some MLOps runners hard-set
safe_mode=True
Code execution in ML workload context
- Loader process holds valuable secrets/creds/data
- No pod-level egress restrictions
- Well-run ML platforms scope IAM per job with short-lived tokens
- Network policy blocks pod-initiated egress to the internet
The supporting signals.
| CVE | CVE-2026-12484 — keras-team/keras 3.15.0 Lambda deserialization RCE |
|---|---|
| CWE | CWE-502 (Deserialization of Untrusted Data) |
| CVSS v3.0 | AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H = 7.8 HIGH — the UI:R and AV:L components are the real gating factors |
| KEV status | Not listed as of 2026-07-20 |
| EPSS | Not yet scored (disclosed 2026-07-19); sibling CVE-2026-12481 tracks in the low single digits — expect the same class |
| Exploit maturity | Public PoC exists for the Lambda gadget class — Huntr blog and HiddenLayer advisory publish working payload structures |
| In-the-wild | No confirmed campaigns targeting 12484 specifically; broader Keras deserialization abuse observed in ML supply-chain poisoning research |
| Affected | keras 3.15.0; likely regression of the 3.14.0 fix for CVE-2026-12481 |
| Fixed in | Track keras-team/keras releases post-2026-07-19; pin to safe_mode=True as an interim |
| Reporter | Huntr researcher community — same corpus that produced CVE-2026-12481 |
| Exposure telemetry | Not a network-facing service — Shodan/GreyNoise irrelevant; scan surface is *artifact provenance*, not TCP ports |
noisgate verdict.
The decisive factor is the UI:R + AV:L combination — exploitation requires a user or automated pipeline to deliberately load an attacker-supplied .keras file, which for the median enterprise fleet is a supply-chain hygiene problem, not a remote-exploitation event. It sits at MEDIUM rather than HIGH because there is no wormable path and no listening service; if your org runs a public model-ingestion MLOps platform, treat it as HIGH under the role multiplier.
Why this verdict
- Friction — user interaction required: No
.kerasfile gets deserialized without a human or pipeline callingload_model(). Removes drive-by and network-reachable exploitation. - Friction — local attack vector: Nothing listens on a port. The attacker needs a delivery channel (model hub, phishing, poisoned artifact registry). That is one full compromise step ahead of most attackers.
- Friction — narrow install base at risk: Only orgs on exactly the vulnerable Keras release AND loading untrusted models are exposed. Most production ML shops pin Keras versions per-project and load only in-house artifacts.
- Role multiplier — data scientist workstation: Chain succeeds; blast radius is one laptop's creds and any cloud tokens it holds. Bad but contained. Keeps floor at MEDIUM.
- Role multiplier — public MLOps ingestion platform (HuggingFace-style, model marketplace, SaaS fine-tuning): Chain succeeds trivially because untrusted models are the *product*. Blast radius is tenant-scale — customer data, model IP, pipeline credentials. For these orgs the floor rises to HIGH, but this is <10% of the Keras install base, so it does not push the global verdict to CRITICAL.
- No KEV, no observed campaigns, no network reachability — nothing forces the mitigation window shorter than the standard MEDIUM SLA.
Why not higher?
HIGH would require either KEV listing, evidence of in-the-wild exploitation, or the affected component being canonically a high-value-role service. Keras is a library, not an identity/CA/hypervisor/edge appliance, and the exploitation model demands cooperative user action. Neither the population nor the reachability supports HIGH for the median enterprise.
Why not lower?
LOW / IGNORE would be wrong because working PoCs exist for the class, RCE is confirmed rather than theoretical, and the ML supply chain is a real and rising attack surface. Any org loading community models on this version is genuinely exposed.
What to do — in priority order.
- Force
safe_mode=Trueat everyload_modelcall site — Grep your codebase forkeras.models.load_model,keras.layers.deserialize, andclone_model; wrap inwith keras.config.SafeModeScope(True):or passsafe_mode=Trueexplicitly. Do this within the noisgate MEDIUM window — no mitigation SLA, but this is a 1-hour lift that should not wait. - Pin Keras away from 3.15.0 — In
requirements.txt/pyproject.toml/Pipfile, downgrade to the last known-good 3.x release or hold at the patched release once published. Trigger a full CI re-lock and rebuild container images. - Enforce model provenance at the artifact registry — Only allow
.kerasartifacts signed with an internal key (Sigstore / cosign) into your MLflow/W&B/S3 model registry. Block ad-hocwgetof third-party checkpoints from developer environments via egress proxy policy. - Deploy YARA scanning on model archives — Use HiddenLayer's public rules (or roll your own detecting marshalled bytecode in
config.jsoninside.keraszips) at your artifact gate and email gateway. - Segment ML training/inference IAM — Ensure Jupyter/Airflow/KubeFlow workers hold only short-lived, least-privilege cloud tokens. This caps blast radius when the RCE does fire.
- WAF / network IPS — the bug fires client-side on file load, no HTTP signature to match.
- Perimeter patching / edge appliances — the vulnerability lives in a Python library on internal ML workloads, not on any network device.
- Blocking
.kerasfile extension at email gateway alone — attackers will rename to.zip,.tar, or serve over HTTPS from a hub the org allowlists. - Relying on
safe_modedefault — that's exactly the code path that fails open here.
Crowdsourced verification payload.
Run on any host with the Python environment you want to audit. Invoke as python check_cve_2026_12484.py from inside the target virtualenv / conda env / container. No elevated privileges required — this is a pure package-introspection check.
#!/usr/bin/env python3
# noisgate verification: CVE-2026-12484 keras Lambda deserialization RCE
# Exit codes: 0=PATCHED, 1=VULNERABLE, 2=UNKNOWN
import sys
VULN_VERSIONS = {"3.15.0"}
# Add the fixed version here once vendor publishes it
FIXED_MIN = None # e.g., "3.15.1"
def parse(v):
try:
return tuple(int(x) for x in v.split(".")[:3])
except Exception:
return None
try:
import keras
except ImportError:
print("UNKNOWN: keras not installed in this environment")
sys.exit(2)
ver = getattr(keras, "__version__", None)
if not ver:
print("UNKNOWN: cannot determine keras version")
sys.exit(2)
print(f"Detected keras version: {ver}")
if ver in VULN_VERSIONS:
print(f"VULNERABLE: keras {ver} is affected by CVE-2026-12484")
sys.exit(1)
if FIXED_MIN and parse(ver) and parse(FIXED_MIN):
if parse(ver) >= parse(FIXED_MIN):
print(f"PATCHED: keras {ver} >= fixed baseline {FIXED_MIN}")
sys.exit(0)
# Sanity: probe safe_mode guard behaviour
try:
from keras.src.saving.serialization_lib import SafeModeScope # noqa
print("PATCHED (likely): version not in known vulnerable set; confirm against vendor advisory")
sys.exit(0)
except Exception:
print("UNKNOWN: could not introspect SafeModeScope; verify manually")
sys.exit(2)
If you remember one thing.
safe_mode=True hardening is a one-liner — do it this week alongside adding YARA scanning to your model registry gate. The noisgate remediation SLA gives you 365 days to fully upgrade to the vendor's fixed release, but if you operate a public model-ingestion platform (HuggingFace-style, MLOps SaaS, fine-tuning marketplace) treat this as HIGH per the role multiplier and compress mitigation to ≤30 days and remediation to ≤180 days. No KEV, no observed exploitation — no need to page anyone tonight.Sources
- Huntr — Hunting Vulnerabilities in Keras Model Deserialization
- OffSeq Threat Radar — CVE-2026-12481 (sibling class)
- o3.security — CVE-2026-12481 deserialization writeup
- HiddenLayer — keras.models.load_model .h5 advisory
- BlueRock — Neutralizing Deserialization Attacks Against AI/ML Workloads
- HackTricks — Keras Model Deserialization RCE and Gadget Hunting
- Snyk — Keras CWE-502 tracker
- CWE-502 — Deserialization of Untrusted Data
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.