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.


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.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #636

Related Articles

critical

deleteNestedProperty Prototype Pollution via Dot-Notation Path

The `deleteNestedProperty` function in propertyUtils.ts allowed attackers to manipulate JavaScript object prototypes by passing specially crafted dot-notation paths like `__proto__.polluted`. A fix now blocks dangerous keys before processing, preventing prototype pollution attacks that could affect all objects in the application.

high

How Denial of Service via Infinite Loop Happens in JavaScript Dependencies and How to Fix It

CVE-2026-67213 is a high-severity denial of service vulnerability in nanoid before version 5.1.6 that triggers an infinite loop during random ID generation when processing specially crafted input. We upgraded nanoid across the entire dependency tree to patch this flaw and prevent attackers from freezing application threads. This fix ensures that ID generation remains resilient even when handling adversarial input patterns.

high

How Sensitive Data Exposure happens in Zotero plugins and how to fix it

A high-severity data exposure vulnerability in `Zotero.ts` automatically transmitted complete document metadata—including private notes, attachment paths, and tags—to external LLM services without user consent. The fix replaces broad `item.toJSON()` serialization with explicit field selection, sending only essential bibliographic data.

high

How missing dependency update cooldowns happen in GitHub Dependabot configurations and how to fix it

A semgrep scan flagged `.github/dependabot.yml` for lacking a cooldown period, meaning Dependabot would immediately propose updates to brand-new package versions across npm, Bundler, and Docker ecosystems. The fix adds a `cooldown: default-days: 7` block to every `package-ecosystem` entry, forcing a one-week waiting period before newly published releases are considered — reducing exposure to malicious or unstable package drops.

high

How Path Traversal Happens in TensorFlow's Data Service and How to Fix It

TensorFlow's data service dispatcher validated dataset IDs against forward-slash traversal attacks but overlooked backslash characters on non-Windows platforms, allowing attackers to escape the root directory. A targeted fix adds explicit backslash validation across all platforms, closing a high-severity path traversal vulnerability in the snapshot management system.

critical

How Unbounded WebSocket Message Handling Causes Resource Exhaustion in Node.js and How to Fix It

The WebSocketCrossServerAdapter class in a popular Node.js WebSocket library lacked any rate limiting on inbound messages, allowing attackers to flood Redis nodes and WebSocket servers with high-volume traffic. The fix introduces a configurable `rateLimit` option that caps messages per connection per second, preventing resource exhaustion while preserving legitimate functionality.