Your Keras Model File Is Executable Code, Not Data A Keras model file is executable code, not data, because its config can carry marshalled CPython bytecode via Lambda layers, and Keras's safe_mode has been bypassed repeatedly, with CVEs including CVE-2025-1550 (fixed in Keras 3.9), CVE-2025-8747, CVE-2025-9905, and CVE-2026-12481, according to the AIsbom project and JFrog researchers. The format ships executable bytecode by design, and safe_mode bypasses allow arbitrary code execution on load, even with safe_mode enabled. Teams should treat .keras and .h5 files as code, not data, and scan them without deserializing. Security https://sourcefeed.dev/c/security Article Your Keras Model File Is Executable Code, Not Data safe mode keeps getting bypassed because the format was built to run whatever the config names. Emeka Okafor https://sourcefeed.dev/u/emeka okafor Everyone who has spent time hardening an ML pipeline knows the pickle sermon by heart. torch.load calls reduce , reduce runs whatever the attacker wrote, and a checkpoint pulled from a random Hugging Face repo becomes remote code execution the instant you deserialize it. The lesson most teams took away was simple: stop shipping pickles. Move to .h5 , move to .keras , move to formats that "just store weights." That lesson is comforting, widely repeated, and wrong in a way that keeps producing CVEs. The uncomfortable detail — resurfaced this week by the AIsbom https://dev.to/aisbom project and grounded in a year of real advisories — is that a Keras model config can carry a marshalled Python code object. Not a pickle. Not a weight tensor. A serialized chunk of CPython bytecode, sitting inside a JSON-ish config, waiting for a loader to hand it back to the interpreter. Why a "config" contains bytecode The mechanism is boring, which is exactly why it's dangerous. Keras lets you define a layer as an arbitrary Python callable through Lambda https://keras.io/api/layers/core layers/lambda/ . That's a genuinely useful feature — a quick Lambda lambda x: x 2 beats writing a full layer subclass. But a lambda is code, and code has to survive being written to disk. Keras solves this the only way it can: it runs the callable through marshal , stores the resulting code object in the model config, and reconstitutes it on load.So the "safe" format ships executable bytecode by design. This isn't a bug report against Keras; it's documented behavior. The bug is in everyone's mental model, where .keras and .h5 got filed under "data" instead of "code." Marshalled bytecode is arguably worse to defend against than pickle, for a subtle reason the AIsbom writeup nails: the obvious way to inspect a marshalled blob is to unmarshal it, and marshal.loads on untrusted input is itself unsafe. The scanner and the victim reach for the same dangerous primitive. Any tool that wants to flag these payloads has to classify them from header bytes without ever calling the deserializer — which is what the new release does, and what most naive "just scan the model" pipelines don't. safe mode was supposed to fix this. It keeps not fixing this. Keras did respond. Since 2.13, safe mode=True is the default, and it blocks deserialization of Lambda layers carrying marshalled code. If the story ended there, this would be a footnote. Instead, safe mode has become one of the most reliably bypassed security controls in the ML tooling world, and the CVE trail tells the story better than any threat model: : You don't even need a Lambda layer. Hand-edit CVE-2025-1550 https://github.com/advisories/GHSA-48g7-3x6r-xfhp config.json inside the .keras archive to name an arbitrary Python module and function with arguments, and load model invokes it — with safe mode=True , with no call to the model, on load alone. Fixed in Keras 3.9 March 2025 by restricting imports to Keras's own modules.: That import restriction gets bypassed by reusing internal Keras functionality as a gadget chain. JFrog's researchers pointed at CVE-2025-8747 https://github.com/advisories/GHSA-c9rc-mg46-23w3 keras.utils.get file , which will happily download an attacker-chosen URL into an attacker-chosen path — say, your ~/.ssh/ directory.: Load a legacy HDF5 model and CVE-2025-9905 https://github.com/advisories/GHSA-c9rc-mg46-23w3 safe mode is silently ignored. No warning, no error, full execution. CVE-2026-12481 : The guard conflates safe mode=None unset, should default to deny with safe mode=False explicitly off , so passing None sails straight past the check. Four bypasses, four different root causes, one pattern: safe mode is an allowlist bolted onto a deserializer that was designed to reconstruct arbitrary Python objects. Every patch narrows one path while the underlying capability — "the config can name code to run" — stays intact. This is the textbook signature of a design that treats security as a filter instead of a boundary. The format-swap advice was never the fix Here's the editorial line, and I'll defend it: telling developers to "switch off pickle" was security theater dressed as best practice. The threat was never the pickle opcode format specifically; it was the decision to make deserialization and code loading the same operation. .keras , .h5 , ONNX external-data paths that read arbitrary files, custom operator domains , and GGUF embedded Jinja templates you can't safely render because of known sandbox escapes all reproduce that decision in their own dialect. The one format that actually broke the pattern is safetensors https://github.com/huggingface/safetensors , and it's worth understanding why it's different rather than cargo-culting it as the next magic extension. Safetensors stores tensors and nothing else — no callables, no config, no code path from file to interpreter. That's the real fix: not a safer serializer, but a format that structurally cannot express executable content. The catch developers hit in practice is that weights alone don't reconstruct a model. You still need the architecture, and the moment that architecture description can name layers, functions, or custom objects, you've quietly re-opened the door safetensors closed. What to actually do on Monday Concrete, in priority order: Treat every model artifact as untrusted code, not data. If you wouldn't curl | bash a stranger's script, don't load model their .keras in your training environment. That reframing does more than any single patch. Load untrusted models in a sandbox — a container with no network egress, no credentials mounted, a throwaway user, seccomp on. Assume safe mode will be bypassed, because the record says it will be. Upgrade, but don't trust the upgrade. Keras 3.9+ closes 1550 and its known descendants; nothing closes the next get file -style gadget nobody's published yet. Patching is necessary and insufficient. Scan without deserializing. Header-byte inspection, not marshal.loads or pickle.load , is the only safe way to classify a blob you don't trust. Any scanner that opens the payload to check the payload is part of the attack surface. Pin provenance. Hashes and signatures on model artifacts, the same way you'd pin a dependency. A model from an untraceable Hugging Face upload deserves exactly the scrutiny of an unsigned binary from an unknown vendor — which is to say, run it nowhere you care about. The pickle era taught the ML world that model files can be weapons. The Keras safe mode saga is teaching a harder second lesson: swapping the file extension doesn't disarm them. As long as a model's architecture can name code, loading a model is running a program — and the only durable defense is to run it like you'd run any other untrusted program. Sources & further reading - Your Keras model config can contain a marshalled Python code object https://dev.to/aisbom/your-keras-model-config-can-contain-a-marshalled-python-code-object-5885 — dev.to - Is TensorFlow Keras Safe Mode Actually Safe? Bypassing safe mode to Achieve Arbitrary Code Execution https://jfrog.com/blog/keras-safe mode-bypass-vulnerability/ — jfrog.com - Arbitrary Code Execution via Crafted Keras Config for Model Loading CVE-2025-1550 https://github.com/advisories/GHSA-48g7-3x6r-xfhp — github.com - Keras vulnerable to CVE-2025-1550 bypass via reuse of internal functionality CVE-2025-8747 https://github.com/advisories/GHSA-c9rc-mg46-23w3 — github.com - CVE-2025-9905 - Bypassing Keras safe mode via Legacy HDF5 File Format https://github.com/io-no/CVE-Reports/issues/7 — github.com Emeka Okafor https://sourcefeed.dev/u/emeka okafor · Security Editor Emeka has spent over a decade tracking threat actors, vulnerability disclosures, and the evolving landscape of application security, bringing a sharp continent-spanning perspective to his reporting. He's known for translating dense CVE advisories into clear, actionable context that developers and security teams alike actually read. Discussion 0 No comments yet Be the first to weigh in.