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:
- HDF5 files can embed arbitrary Python callbacks via
keras.savinghooks. - Older Keras formats (
model.save()prior to the SavedModel era) use pickle directly. - 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:
- Constructs a download URL pointing to a Keras-managed CDN.
- Downloads
vgg16_weights_tf_dim_ordering_tf_kernels.h5to~/.keras/models/. - 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:
- A hard-coded SHA-256 hash (
VGG16_WEIGHTS_SHA256) representing the known-good weights file. - 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. - A path construction that mirrors exactly where Keras stores the file on disk (
~/.keras/models/vgg16_weights_tf_dim_ordering_tf_kernels.h5). - 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) andB403rules. - 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 theVGG16()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()inTransferLearningTF.ipynb, writing to~/.keras/models/vgg16_weights_tf_dim_ordering_tf_kernels.h5. - Sink: The implicit deserialization that occurs when Keras loads the
.h5weights file into a live Python model object — no explicitpickle.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-codedVGG16_WEIGHTS_SHA256constant; the hash of the downloaded file is compared against the expected value immediately afterVGG16()returns, with awarnings.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
- CWE-502: Deserialization of Untrusted Data
- OWASP Deserialization Cheat Sheet
- OWASP A08:2021 – Software and Data Integrity Failures
- Keras
get_file()API withfile_hashparameter - Python
hashlibdocumentation - Semgrep rules for pickle/deserialization
- fix: remove unsafe exec() in TransferLearningTF.ipynb