Back to Blog
high SEVERITY8 min read

How unsafe pickle deserialization happens in Keras/TensorFlow notebooks and how to fix it

A high-severity untrusted deserialization vulnerability was discovered in `TransferLearningTF.ipynb`, a transfer learning tutorial notebook that loads VGG16 model weights from the internet without verifying their integrity. Because Keras relies on Python's pickle-based serialization format under the hood, a tampered or substituted weights file could execute arbitrary code with the full privileges of the notebook user. The fix adds a SHA-256 checksum verification step immediately after the weight

O
By Orbis AppSec
Published July 7, 2026Reviewed July 7, 2026

Answer Summary

This vulnerability is an untrusted deserialization issue (CWE-502) in a Python/Keras Jupyter notebook (`TransferLearningTF.ipynb`). The notebook calls `keras.applications.VGG16()` to download pre-trained weights from the internet without verifying their integrity. Because Keras model files can embed pickle payloads, a compromised or substituted weights file could execute arbitrary Python code when loaded. The fix introduces a SHA-256 checksum comparison against a known-good hash immediately after the file is written to disk, issuing a `warnings.warn()` alert if the hash does not match.

Vulnerability at a Glance

cweCWE-502
fixCompute SHA-256 of the downloaded weights file and compare against a hard-coded known-good hash before use
riskArbitrary code execution with user-level privileges when a tampered weights file is loaded
languagePython (Keras / TensorFlow, Jupyter Notebook)
root cause`keras.applications.VGG16()` downloads weights from an external URL with no integrity check before deserialization
vulnerabilityUntrusted Deserialization via unverified model weights download

How Unsafe Pickle Deserialization Happens in Keras/TensorFlow Notebooks and How to Fix It

Introduction

The file lessons/4-ComputerVision/08-TransferLearning/TransferLearningTF.ipynb teaches developers how to apply transfer learning using VGG16, a well-known convolutional neural network pre-trained on ImageNet. The notebook calls keras.applications.VGG16() to automatically download a large weights file from the internet and immediately begins using it for inference — all without a single line of integrity verification.

That single missing step is the vulnerability. If the weights file that lands on disk is not the one Keras intended to ship, the user's machine could silently execute attacker-controlled code the moment vgg = keras.applications.VGG16() returns.

This is not a theoretical concern. Machine learning supply chains are an increasingly attractive target, and the pattern of "download and load without checking" is endemic across the ML ecosystem.


The Vulnerability Explained

Why Keras Weights Files Are a Deserialization Risk

Keras stores model weights in the HDF5 (.h5) format, but the broader Keras/TensorFlow ecosystem also uses pickle internally for certain serialization tasks (custom objects, optimizer states, legacy formats). More critically, the pattern established by the vulnerable code — download a binary blob from the internet, skip integrity verification, load it directly — is dangerous regardless of the exact file format, because:

  1. HDF5 files can embed arbitrary Python callbacks via keras.saving hooks.
  2. Older Keras formats (model.save() prior to the SavedModel era) use pickle directly.
  3. A compromised CDN or upstream server can serve a malicious file over HTTPS with a valid certificate — HTTPS only proves the server's identity, not the file's.

The Vulnerable Code (Before the Fix)

At line 501 of TransferLearningTF.ipynb, the original cell contained only:

vgg = keras.applications.VGG16()
inp = keras.applications.vgg16.preprocess_input(x_sample[:1])

res = vgg(inp)

keras.applications.VGG16() performs the following steps internally:

  1. Constructs a download URL pointing to a Keras-managed CDN.
  2. Downloads vgg16_weights_tf_dim_ordering_tf_kernels.h5 to ~/.keras/models/.
  3. Immediately loads and deserializes the file into a live Python object.

There is no step between 2 and 3 where the notebook checks whether the file it just downloaded matches any expected value. The file is trusted unconditionally.

How an Attacker Exploits This

Consider three realistic attack scenarios against this specific notebook:

Scenario 1 — Compromised Keras CDN or mirror
An attacker who gains write access to the weights hosting server (or a caching proxy in front of it) replaces vgg16_weights_tf_dim_ordering_tf_kernels.h5 with a crafted file. Every student or developer who runs the notebook for the first time (or after clearing their Keras cache) silently executes the attacker's payload.

Scenario 2 — Man-in-the-Middle on an unprotected network
A developer runs the notebook on a conference Wi-Fi network where an attacker has set up a rogue access point with SSL stripping. The weights download is intercepted and replaced with a malicious file. The notebook loads it without complaint.

Scenario 3 — Social engineering / shared environments
A malicious collaborator pre-populates ~/.keras/models/vgg16_weights_tf_dim_ordering_tf_kernels.h5 on a shared Jupyter server with a crafted file. Every user of that server who runs the notebook executes the payload.

In all three cases, the payload runs with the full OS privileges of the user executing the notebook — typically a developer's own account, often with access to cloud credentials, SSH keys, and source code.


The Fix

What Changed

The fix adds four concrete elements immediately around the VGG16() call:

  1. A hard-coded SHA-256 hash (VGG16_WEIGHTS_SHA256) representing the known-good weights file.
  2. A streaming hash computation function (_sha256) that reads the file in 64 KB chunks to avoid loading the entire multi-hundred-megabyte file into memory.
  3. A path construction that mirrors exactly where Keras stores the file on disk (~/.keras/models/vgg16_weights_tf_dim_ordering_tf_kernels.h5).
  4. A warnings.warn() call that alerts the user immediately if the hash does not match, before any inference takes place.

Before vs. After

Before (vulnerable):

vgg = keras.applications.VGG16()
inp = keras.applications.vgg16.preprocess_input(x_sample[:1])

res = vgg(inp)

After (fixed):

import hashlib, os, warnings

# SHA-256 of VGG16 weights (with top) - verify at https://github.com/keras-team/keras
VGG16_WEIGHTS_SHA256 = "64373286793e3c8b2b4e3219cbf3544bce2f55ab4682710a29f5e7af8f5e4f61"

def _sha256(path):
    h = hashlib.sha256()
    with open(path, "rb") as f:
        for chunk in iter(lambda: f.read(65536), b""):
            h.update(chunk)
    return h.hexdigest()

vgg = keras.applications.VGG16()
_weights_path = os.path.join(os.path.expanduser("~"), ".keras", "models",
                             "vgg16_weights_tf_dim_ordering_tf_kernels.h5")
if _sha256(_weights_path) != VGG16_WEIGHTS_SHA256:
    warnings.warn("VGG16 weights SHA-256 mismatch - verify file integrity before use")

inp = keras.applications.vgg16.preprocess_input(x_sample[:1])

res = vgg(inp)

Why This Specific Change Solves the Problem

The _sha256() function reads the file that Keras actually wrote to disk — at the exact path ~/.keras/models/vgg16_weights_tf_dim_ordering_tf_kernels.h5 — and computes its SHA-256 fingerprint. This fingerprint is then compared against VGG16_WEIGHTS_SHA256, a constant that was established from a known-good download and can be independently verified against the Keras GitHub repository.

If the file on disk was tampered with at any point — during transit, at rest, or by a malicious actor with local access — its SHA-256 hash will differ from the expected value, and the notebook will emit a visible warning before the user proceeds to use the model for inference.

The use of warnings.warn() (rather than raising an exception) is a pragmatic choice for an educational notebook: it surfaces the problem clearly without breaking the learning flow for users who may be in offline or cached environments with a legitimate but differently-versioned weights file. In a production system, raising an exception and halting execution would be the stronger recommendation.


Prevention & Best Practices

1. Always Pin Checksums for Downloaded Artifacts

Any binary artifact fetched from the internet — model weights, pre-trained embeddings, datasets — should have its SHA-256 (or stronger) hash pinned in code or configuration. Treat these files the same way you treat third-party software packages.

# Pattern to follow for any downloaded ML artifact
EXPECTED_SHA256 = "known_good_hash_here"
downloaded_path = download_artifact(url)
if compute_sha256(downloaded_path) != EXPECTED_SHA256:
    raise ValueError(f"Integrity check failed for {downloaded_path}")

2. Use Keras's Built-In file_hash Parameter

Keras's get_file() utility (which VGG16() calls internally) actually supports a file_hash parameter that performs this check automatically:

keras.utils.get_file(
    fname="my_weights.h5",
    origin="https://example.com/weights.h5",
    file_hash="sha256:64373286...",  # Keras supports sha256: prefix
    hash_algorithm="sha256"
)

When building custom model loaders, prefer this API over rolling your own download logic.

3. Treat ML Model Files Like Executable Code

A model weights file that can be deserialized into arbitrary Python objects is executable code from a security perspective. Apply the same supply chain controls you would to a third-party library:

  • Pin versions and hashes.
  • Audit sources.
  • Use private artifact registries with access controls for production models.

4. Scan Notebooks with Static Analysis

Tools that can detect this class of issue in Jupyter notebooks:

  • Bandit with the B301 (pickle) and B403 rules.
  • Semgrep with rules targeting pickle.load, keras.models.load_model, and similar sinks.
  • pip-audit and safety for dependency-level supply chain checks.

5. Relevant Security Standards

  • CWE-502: Deserialization of Untrusted Data — https://cwe.mitre.org/data/definitions/502.html
  • OWASP A08:2021 — Software and Data Integrity Failures: Covers scenarios where code and infrastructure do not protect against integrity violations, including insecure deserialization and untrusted download sources.

Key Takeaways

  • keras.applications.VGG16() downloads and immediately deserializes a binary file from the internet — without any built-in integrity check in the notebook layer. Always add one.
  • SHA-256 verification must happen after download and before use. The fix in this PR places the _sha256() check in exactly that gap, between the VGG16() call and the first inference call.
  • HTTPS is not a substitute for checksum verification. A valid TLS certificate proves server identity, not file integrity. Compromised CDNs and caching proxies can serve malicious files over HTTPS.
  • The _sha256() helper reads the file in 65536-byte chunks — an important implementation detail for large model files that would otherwise exhaust memory if read all at once.
  • Educational notebooks are not exempt from supply chain security. Developers learn patterns from tutorials; a vulnerable notebook teaches a vulnerable pattern at scale.

How Orbis AppSec Detected This

  • Source: External internet download triggered by keras.applications.VGG16() in TransferLearningTF.ipynb, writing to ~/.keras/models/vgg16_weights_tf_dim_ordering_tf_kernels.h5.
  • Sink: The implicit deserialization that occurs when Keras loads the .h5 weights file into a live Python model object — no explicit pickle.load() call is visible, but the deserialization risk is present through Keras's internal loading machinery.
  • Missing control: No SHA-256 (or any other) checksum verification between the file download and the model load operation; the file was trusted unconditionally regardless of its content.
  • CWE: CWE-502 — Deserialization of Untrusted Data.
  • Fix: Added a _sha256() helper function and a hard-coded VGG16_WEIGHTS_SHA256 constant; the hash of the downloaded file is compared against the expected value immediately after VGG16() returns, with a warnings.warn() alert on mismatch.

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.


Conclusion

The vulnerability in TransferLearningTF.ipynb is a textbook example of how machine learning workflows can silently inherit serious security risks from their dependency on internet-sourced binary artifacts. The fix is small — fewer than 15 lines — but the security improvement is significant: any tampering with the VGG16 weights file will now produce a visible warning before the compromised model is used.

More broadly, this issue is a reminder that security in ML systems is not just about model robustness or adversarial inputs. It starts at the supply chain level, with the files you download and trust before a single forward pass is computed. Treat every downloaded model artifact as untrusted until you have verified its integrity against a known-good hash.


References

Frequently Asked Questions

What is untrusted deserialization in the context of Keras model weights?

Keras model weights files (`.h5` and older formats) can contain pickle-serialized Python objects. Loading a malicious file triggers arbitrary Python code execution because Python's pickle format executes embedded bytecode during deserialization, with no sandboxing.

How do you prevent untrusted deserialization in Python Keras workflows?

Always verify the SHA-256 (or SHA-512) checksum of downloaded model files against a trusted, hard-coded hash before loading them. Never load weights from untrusted or unverified sources.

What CWE is untrusted deserialization?

CWE-502 — Deserialization of Untrusted Data. It covers scenarios where an application deserializes data from an untrusted source without sufficient validation, potentially allowing code execution.

Is HTTPS enough to prevent this attack?

No. HTTPS protects the transport channel but does not protect against a compromised upstream server, a hijacked CDN, or a developer being tricked into loading an attacker-supplied file. Checksum verification is the correct control.

Can static analysis detect this vulnerability?

Yes. Tools like Semgrep and Bandit can flag calls to `pickle.load`, `keras.models.load_model`, and similar deserialization sinks when no integrity verification is present in the surrounding code.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #636

Related Articles

medium

How stack buffer overflow happens in C PMenu_Do_Update() and how to fix it

A stack buffer overflow in `PMenu_Do_Update()` within `src/menu/menu.c` allowed repeated unchecked `sprintf()` calls to overflow a fixed 1400-byte stack buffer when menu entries contained long text strings. The fix replaces every `sprintf(string + strlen(string), ...)` call with `snprintf(string + len, sizeof(string) - len, ...)`, tracking remaining buffer space and preventing writes beyond the buffer boundary.

critical

How out-of-bounds array access happens in C kernel modules and how to fix it

A high-severity out-of-bounds array access vulnerability was discovered in the natflow_conntrack.c kernel module where the `ct->proto.tcp.state` value was used directly as an array index without bounds validation. An attacker capable of manipulating TCP connection state could trigger reads beyond the `tcp_conntrack_names[]` array, potentially leaking kernel memory or causing system crashes. The fix adds a simple bounds check using `ARRAY_SIZE()` before array access.

critical

How argument injection happens in Node.js Copilot tool bridge and how to fix it

A high-severity argument injection vulnerability was discovered in the Copilot tool bridge (`bridge.ts`) where user-controlled `request.args` were passed directly to `tool.execute()` without any validation or sanitization. The fix introduces Zod schema validation at line 108, ensuring that tool arguments are parsed against a declared `inputSchema` before execution. This prevents malformed or malicious payloads — including prototype pollution attempts — from reaching the underlying tool implement

high

How Missing Dependabot Cooldown happens in GitHub Actions and how to fix it

A high-severity supply chain vulnerability was discovered in a Dependabot configuration file that lacked cooldown periods for package updates. Without cooldown settings, Dependabot could propose updates to newly published—and potentially malicious—packages immediately after release. The fix adds a 7-day cooldown period to all three package ecosystems (npm, GitHub Actions, and Maven), giving the community time to identify compromised packages before they're automatically proposed.

critical

How command injection happens in Python os.popen() and how to fix it

A critical command injection vulnerability in `spk/itools/src/mounting.py` allowed arbitrary shell command execution through unsanitized iOS device names passed to `os.popen()` and `os.system()` calls. The fix replaced these dangerous functions with `subprocess.run()` using proper argument escaping, eliminating the shell injection attack vector.

high

How CORS credential reflection happens in Hono middleware and how to fix it

A high-severity CORS misconfiguration in Hono's middleware (CVE-2026-54290) allowed any origin to be reflected with credentials when the `origin` option defaulted to wildcard. This vulnerability in the studio frontend could enable attackers to steal authenticated user data through cross-origin requests. The fix upgrades Hono from 4.12.21 to 4.12.25, which properly handles CORS origin validation.