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

critical

How arbitrary code execution via injected protobuf definition type fields happens in Node.js and how to fix it

A critical arbitrary code execution vulnerability (CVE-2026-41242) was discovered in protobufjs versions prior to 7.5.5 and 8.0.1, allowing attackers to inject malicious code through crafted protobuf definition type fields. The fix upgrades the dependency from the vulnerable version 6.11.4 to 7.5.5, which properly sanitizes type field inputs during protobuf parsing. This vulnerability is especially dangerous because protobufjs is widely used in Node.js applications for serialization, meaning a s

high

How command injection happens in Node.js child_process and how to fix it

A high-severity command injection vulnerability was discovered in `bootstrap.js` at line 34, where the `exec()` function was used to run shell commands constructed from a `folder` variable. By replacing `exec()` with `execFile()` and passing arguments as an array, the fix eliminates the shell interpolation entirely, closing the door on command injection attacks that could have affected all downstream consumers of this Node.js library.

high

How unsafe pickle deserialization happens in NumPy's np.load() and how to fix it

A high-severity arbitrary code execution vulnerability was discovered in `tools/ardy-engine/retarget.py` where `np.load()` was called with `allow_pickle=True`, enabling attackers to embed malicious pickle payloads in `.npz` files. The fix was a single-character change—switching `allow_pickle=True` to `allow_pickle=False`—that eliminates the deserialization attack vector while preserving the file's legitimate array data loading functionality.

high

How SSRF and Credential Leakage via Absolute URLs happens in axios and how to fix it

A high-severity vulnerability (CVE-2025-27152) in axios versions prior to 1.8.2 allowed Server-Side Request Forgery (SSRF) attacks and credential leakage when making HTTP requests with absolute URLs. This vulnerability was fixed by upgrading from axios 1.7.4 to 1.8.2 in both package.json and package-lock.json, eliminating the attack vector that could have exposed authentication tokens and enabled unauthorized server-side requests.

high

How pickle-based arbitrary code execution happens in PyTorch and how to fix it

A high-severity arbitrary code execution vulnerability was discovered in `scripts/export_joyvasa_audio.py` where `torch.load()` was called with `weights_only=False`, allowing any pickle-serialized Python object — including malicious code — to execute during checkpoint loading. The fix switches to `weights_only=True` and explicitly allowlists only the two non-standard classes the checkpoint actually requires: `argparse.Namespace` and `pathlib.PosixPath`. This closes a real code execution path tha

critical

How WebSocket protocol length header abuse happens in Node.js and how to fix it

A critical vulnerability (CVE-2026-54466) in websocket-driver versions prior to 0.7.5 allows attackers to corrupt WebSocket messages by manipulating protocol length headers. This can lead to data integrity issues, denial of service, or potentially arbitrary code execution in affected Node.js applications. The fix involves upgrading the websocket-driver dependency to version 0.7.5.