{"slug": "your-keras-model-file-is-executable-code-not-data", "title": "Your Keras Model File Is Executable Code, Not Data", "summary": "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.", "body_md": "[Security](https://sourcefeed.dev/c/security)Article\n\n# Your Keras Model File Is Executable Code, Not Data\n\nsafe_mode keeps getting bypassed because the format was built to run whatever the config names.\n\n[Emeka Okafor](https://sourcefeed.dev/u/emeka_okafor)\n\nEveryone who has spent time hardening an ML pipeline knows the pickle sermon by heart. `torch.load`\n\ncalls `__reduce__`\n\n, `__reduce__`\n\nruns 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`\n\n, move to `.keras`\n\n, move to formats that \"just store weights.\" That lesson is comforting, widely repeated, and wrong in a way that keeps producing CVEs.\n\nThe 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.\n\n## Why a \"config\" contains bytecode\n\nThe 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\n\n`Lambda(lambda x: x * 2)`\n\nbeats 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`\n\n, 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`\n\nand `.h5`\n\ngot filed under \"data\" instead of \"code.\"\n\nMarshalled 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`\n\non 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.\n\n## safe_mode was supposed to fix this. It keeps not fixing this.\n\nKeras did respond. Since 2.13, `safe_mode=True`\n\nis 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`\n\nhas 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:\n\n: You don't even need a Lambda layer. Hand-edit[CVE-2025-1550](https://github.com/advisories/GHSA-48g7-3x6r-xfhp)`config.json`\n\ninside the`.keras`\n\narchive to name an arbitrary Python module and function with arguments, and`load_model`\n\ninvokes it — with`safe_mode=True`\n\n, 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`\n\n, which will happily download an attacker-chosen URL into an attacker-chosen path — say, your`~/.ssh/`\n\ndirectory.: Load a legacy HDF5 model and[CVE-2025-9905](https://github.com/advisories/GHSA-c9rc-mg46-23w3)`safe_mode`\n\nis silently ignored. No warning, no error, full execution.**CVE-2026-12481**: The guard conflates`safe_mode=None`\n\n(unset, should default to deny) with`safe_mode=False`\n\n(explicitly off), so passing`None`\n\nsails straight past the check.\n\nFour bypasses, four different root causes, one pattern: `safe_mode`\n\nis 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.\n\n## The format-swap advice was never the fix\n\nHere'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`\n\n, `.h5`\n\n, 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.\n\nThe 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.\n\n## What to actually do on Monday\n\nConcrete, in priority order:\n\n**Treat every model artifact as untrusted code, not data.** If you wouldn't`curl | bash`\n\na stranger's script, don't`load_model`\n\ntheir`.keras`\n\nin 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`\n\non. Assume`safe_mode`\n\nwill 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`\n\n-style gadget nobody's published yet. Patching is necessary and insufficient.**Scan without deserializing.** Header-byte inspection, not`marshal.loads`\n\nor`pickle.load`\n\n, 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.\n\nThe pickle era taught the ML world that model files can be weapons. The Keras `safe_mode`\n\nsaga 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.\n\n## Sources & further reading\n\n-\n[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 -\n[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 -\n[Arbitrary Code Execution via Crafted Keras Config for Model Loading (CVE-2025-1550)](https://github.com/advisories/GHSA-48g7-3x6r-xfhp)— github.com -\n[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 -\n[CVE-2025-9905 - Bypassing Keras safe_mode via Legacy HDF5 File Format](https://github.com/io-no/CVE-Reports/issues/7)— github.com\n\n[Emeka Okafor](https://sourcefeed.dev/u/emeka_okafor)· Security Editor\n\nEmeka 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.\n\n## Discussion 0\n\nNo comments yet\n\nBe the first to weigh in.", "url": "https://wpnews.pro/news/your-keras-model-file-is-executable-code-not-data", "canonical_source": "https://sourcefeed.dev/a/your-keras-model-file-is-executable-code-not-data", "published_at": "2026-08-18 21:07:48+00:00", "updated_at": "2026-08-18 21:11:05.451089+00:00", "lang": "en", "topics": ["ai-safety", "ai-policy", "ai-infrastructure"], "entities": ["Keras", "AIsbom", "JFrog", "CVE-2025-1550", "CVE-2025-8747", "CVE-2025-9905", "CVE-2026-12481", "Hugging Face"], "alternates": {"html": "https://wpnews.pro/news/your-keras-model-file-is-executable-code-not-data", "markdown": "https://wpnews.pro/news/your-keras-model-file-is-executable-code-not-data.md", "text": "https://wpnews.pro/news/your-keras-model-file-is-executable-code-not-data.txt", "jsonld": "https://wpnews.pro/news/your-keras-model-file-is-executable-code-not-data.jsonld"}}