Skip to content
datarekha
Infrastructure June 10, 2026

Loading a model file can run code: MLSecOps in 2026

Downloading an untrusted pickle-backed artifact and loading it with an unrestricted or vulnerable deserializer can be as dangerous as running an untrusted script. Real malicious models have shipped on public hubs — here's the ML attack surface and the defenses that belong in your pipeline.

9 min read · by Shreyash Prashu mlopssecuritymlsecopssupply-chainpickle

At 3:07 a.m., a model deployment job downloads refund-v3.pt. The job loads it with torch.load() using one of these unrestricted or vulnerable paths:

  • an old PyTorch release’s unrestricted default;
  • an explicit weights_only=False;
  • an affected pre-2.6 release vulnerable to CVE-2025-32434, even when weights_only=True was requested.

Before the first ticket reaches the model, the runner starts an outbound connection to an unfamiliar host.

That is not a prediction failure. It is a supply-chain incident.

Imagine Northstar, a support company routing 20,000 tickets a day. An engineer finds a promising fine-tuned classifier on a public model hub, downloads it into CI, checks its accuracy, and promotes it. The runner has a cloud token, access to the source tree, and network egress. The model file now has a better position than most attackers ever get.

The mistake is calling a model “data” and treating loading it like opening an image. A model artifact is untrusted input until all of these have been checked:

  • its format;
  • its origin;
  • its contents;
  • its runtime.

For a pickle-backed artifact loaded with an unrestricted or vulnerable deserializer, loading belongs in the same risk category as running an untrusted script.

Unsafe pathUntrusted artifactUnsafe load()Code executionDefended pathUntrusted artifactQuarantine + verifyRegistry gateControlled serving
An untrusted model becomes a code-execution risk at load time; quarantine and promotion controls create a safer serving path.

MLSecOps means applying security and operational controls to the whole machine learning lifecycle:

  • data;
  • training code;
  • model artifacts;
  • deployment;
  • the prediction API.

It is not another dashboard. It is the discipline of putting a real trust boundary around every step.

load() is a trust boundary, not a read operation

Serialization means turning an in-memory object into bytes. Deserialization means reconstructing the object from those bytes. The word sounds passive. The operation is not necessarily passive.

Python’s pickle format can store instructions for reconstructing an object. One of those instructions can identify a callable and the arguments to pass to it. Python’s unpickler may invoke that callable while rebuilding the object. The model does not need to produce a prediction, receive a prompt, or finish loading for the code to run.

That is why a malicious checkpoint can execute code through a method such as __reduce__. The attacker is not hiding a suspicious line in your application. The attacker has put a callable into the bytes your application agreed to interpret.

For a plain state dictionary, the loading boundary should be explicit:

# Use on a currently supported, patched PyTorch release.
checkpoint = torch.load(
    "northstar-refund-v3.pt",
    weights_only=True,
)

The .pt and .pth extensions do not tell you that a file is harmless. Many PyTorch checkpoints are pickle-backed, as are common uses of joblib and cloudpickle. An innocent-looking file name is wearing a name tag, not a security clearance.

PyTorch 2.6 changed torch.load’s default to weights_only=True when no custom pickle_module is supplied. Before 2.6, the default was unrestricted loading, equivalent to weights_only=False.

Do not rely on an implicit default. For a compatible plain state dictionary, pass weights_only=True explicitly on a currently supported, patched release.

weights_only=True can reduce risk by limiting which Python objects the loader will rebuild. It is a useful mitigation, not a universal safety certificate. It may reject legitimate checkpoints. weights_only=False explicitly requests unrestricted deserialization and is unsafe for untrusted artifacts.

Unreviewed allowlists and custom model-code paths have the same trust problem because they expand what the loader may reconstruct or execute. CVE-2025-32434 demonstrated that affected pre-2.6 PyTorch releases could be exploited even when weights_only=True was requested. Upgrade to a supported patched release rather than treating the flag as a sandbox.

The flag also does not protect against:

  • poisoned weights;
  • vulnerable native parsers;
  • custom model code loaded elsewhere in the pipeline.

Check the behavior of the exact PyTorch version in your environment.

The practical rule is simple: loading an untrusted pickle-backed artifact with an unrestricted or vulnerable deserializer should be treated as a code-execution event. Not because every file is malicious, but because you cannot safely know that from the extension or from the fact that the file came from a popular website.

This has already happened on public model hubs

This is not a theoretical concern reserved for a future attacker. Hugging Face’s own security guidance warns that pickle files can enable arbitrary code execution and recommends safer formats such as Safetensors. It also says the Hub scanner is not foolproof.

JFrog reported roughly 100 malicious models on Hugging Face that achieved code execution through pickle’s __reduce__ mechanism when loaded with torch.load. One reportedly carried a reverse-shell payload.

A reverse shell turns “I downloaded a model” into “someone else has an interactive foothold inside the machine that loaded it.”

In early 2025, ReversingLabs described “nullifAI”, a technique involving 7z-compressed, malformed pickle files that could bypass PickleScan’s analysis. The dangerous part could execute before the scanner reached the corruption that made the file look broken.

Separately, the advisory for CVE-2025-1716 documented a remote-code-execution bypass in PickleScan and tracks a fix in PickleScan 0.0.22.

The lesson is not that every scanner is useless. The lesson is that a scanner is a detection layer. It is not a sandbox, and it is not proof that deserializing the file is safe.

The file can be dangerous even when it contains no malware

Suppose Northstar replaces its pickle checkpoint with a perfectly ordinary Safetensors file. That removes one important attack path. It does not remove the rest of the ML attack surface.

Three risks beyond deserialization

Data poisoning means inserting training examples that change the learned behavior. A particularly nasty version is a backdoor: the model behaves normally until it sees a particular phrase, sender pattern, image feature, or token sequence.

If 99,900 ordinary validation tickets are classified correctly and 100 trigger tickets are all routed to the wrong queue, the dashboard can report 99.9 percent accuracy while the attacker’s slice has zero percent accuracy. Aggregate metrics are not a backdoor detector.

Model theft means recovering useful information about a model by querying its API. If Northstar answers ten requests per second, 100,000 queries take about 2.8 hours. That does not guarantee a successful replica; extraction depends on the model, outputs, and query budget.

But returning class probabilities instead of only the necessary decision gives each query more information. The same endpoint may also reveal whether a particular customer record appeared in training.

Evasion means crafting an input that causes a deployed model to make a wrong decision. A ticket can be semantically ordinary to a person but contain unusual spelling, formatting, or instruction-like text that changes the classifier’s output. No malicious model file is required.

The broader supply chain

There is also a broader supply chain. A model repository may contain:

  • weights;
  • configuration;
  • a tokenizer;
  • custom Python modules;
  • adapters;
  • dependency instructions.

Enabling a loader option that executes remote model code can turn a safe-looking weight file into a code-review problem. The model is not one file. It is a small software distribution with an unusually persuasive README.

Build a pipeline that assumes failure

Quarantine and format

The first control is quarantine. Downloaded artifacts should land in an isolated environment with:

  • no cloud credentials;
  • no production network access;
  • an unprivileged account;
  • a read-only filesystem where practical;
  • limits on CPU, memory, disk, and execution time.

A malicious loader can still damage that sandbox, but it has far fewer useful things to steal or destroy.

Do not let the quarantine step share the identity of the serving system. The process that inspects a model should not be able to read customer data. The process that serves approved predictions should not be able to download arbitrary repositories. Separation makes a compromised step a contained incident instead of a tour of the company.

Prefer tensor-only formats such as Safetensors for ordinary weight distribution. For a reviewed architecture, the loading pattern should look conceptually like this:

from safetensors.torch import load_file

state = load_file("northstar-refund-v3.safetensors")
model.load_state_dict(state)

The architecture in that example still has to come from code you reviewed and pinned. Safetensors protects the tensor deserialization boundary; it does not prove that the weights are accurate, fair, or free of a backdoor.

Legacy pickle checkpoints are sometimes unavoidable. An old experiment may contain optimizer state, custom classes, or training objects that a tensor-only format cannot represent.

Load that artifact once, in quarantine, with pinned dependencies and no credentials or egress. Convert only the reviewed weights to a safer distribution format. Record exactly what was converted and test that the converted model produces the expected outputs. “We converted it” is not a provenance record by itself.

Provenance and promotion

Next comes identity. Record:

  • the source repository and revision;
  • the cryptographic digest of the exact bytes;
  • the model architecture;
  • the tokenizer;
  • adapter files;
  • the dataset version;
  • the training-code revision;
  • the dependency lockfile;
  • the base image.

This inventory is an AI bill of materials: a machine-readable account of what went into a model and how to reproduce it. A digest proves that the bytes have not changed since they were recorded. It does not prove that the original bytes were benevolent. Provenance and integrity answer different questions.

Use a model registry and promotion workflow as an enforcement point, not as a ceremonial folder. Production should request an approved model identity, and the deployment system should refuse an unknown digest.

A registry that merely stores whatever someone uploaded is a museum, not a gate.

Data and API controls

Treat data with the same suspicion. Data contracts and quality checks should validate:

  • schema;
  • ranges;
  • missingness;
  • duplicate rates;
  • source proportions;
  • label distributions;
  • unexpected changes in high-risk slices.

Keep a holdout set containing known edge cases and trigger-like inputs. Compare a candidate against the previous model on that set, not just on a random average. No test proves that poisoning is absent, but a model that suddenly changes behavior on a protected slice has earned a pause, not a promotion.

Finally, lock down the prediction API. That means:

  • authenticate callers;
  • apply per-client rate limits and quotas;
  • avoid returning logits or confidence scores unless the product needs them;
  • record enough request metadata to investigate abuse without retaining sensitive content unnecessarily.

Watch for:

  • query bursts;
  • repeated near-duplicates;
  • unusual class-distribution changes;
  • output confidence patterns.

ML observability matters here because extraction and evasion often look like traffic or distribution problems before they look like security incidents.

The strongest objection is sometimes right

A team may reasonably say: “These are our internal checkpoints. We trust the researchers. Pickle preserves custom objects. Our hub scans every upload. Moving everything to Safetensors will break useful workflows.”

That objection is valid for a genuinely controlled environment. If all of these are trusted:

  • the author;
  • the training machine;
  • the dependencies;
  • the artifact path;

accepting the cost of pickle may be rational. Security is not a religious argument against every convenient format.

But “internal” is not the same as “verified.” A compromised laptop, dependency, CI token, or copied experiment can turn that assumption stale without changing the loader call. Scanning improves detection but cannot make a dynamic format non-executable.

And the format trade-off is a reason to isolate legacy loading, not a reason to run it on a privileged build worker.

The right distinction is not safe versus unsafe model. It is trusted and controlled path versus untrusted path. Pickle may survive on the first path. It should not quietly enter the second.

What to do on Monday morning

Start with an inventory, not a policy document. Search application code, notebooks, CI jobs, and deployment images for:

  • torch.load;
  • pickle.load;
  • joblib.load;
  • cloudpickle;
  • direct model-hub downloads;
  • remote-code-loading options.

For each result, record:

  • the artifact source;
  • the loader version;
  • network access;
  • credentials;
  • filesystem permissions.

Then establish one hard invariant: production can serve only a digest that exists in the registry and has an explicit promotion record. Remove direct downloads from the serving startup path. A URL is not an approval workflow.

Create a quarantine job with no secrets and blocked outbound traffic. Run static scans there, but also observe:

  • filesystem writes;
  • child processes;
  • attempted network connections.

Treat an unexpected DNS lookup or subprocess during loading as a security finding, even if the scan reports “clean.”

Migrate the highest-risk legacy checkpoints first:

  • export reviewed tensor weights;
  • pin the architecture and tokenizer;
  • compare predictions against the old checkpoint on a fixed test set;
  • add poisoning-focused slices and API abuse monitoring.

Do not call the migration complete until those steps are done.

The first symptom of pickle malware is often not a bad prediction. It may be a build worker that tries to read a cloud credentials file, spawns an unfamiliar process, or makes an outbound connection while loading.

The first symptom of poisoning is often the opposite: normal aggregate accuracy with one of these behaving strangely:

  • one customer;
  • one phrase;
  • one traffic slice.

Alert on both. You do not need to defeat every exotic adversarial attack before fixing those two very ordinary doors.

For the wider picture, read ML security (MLSecOps). It maps the attack surface beyond model files, while the registry and data-contract guidance turn the main defenses into repeatable release controls.