3 Diffusers CVEs let a Hugging Face model repo run code with trust_remote_code off
Summary. On 27 July 2026 Zafran Security published FaceHugger, research covering three high-severity flaws in Hugging Face's diffusers library: CVE-2026-44827 (CVSS 8.8), CVE-2026-44513 (CVSS 8.8) and CVE-2026-45804 (CVSS 7.5). All three let a crafted Hub repository execute attacker-controlled Python during DiffusionPipeline.from_pretrained, even when trust_remote_code=False or is simply omitted, which is the default. Hugging Face fixed them in diffusers 0.38.0, published 1 May 2026. The library was downloaded more than 8.1 million times in July 2026 alone, so the exposed population is large: diffusers sits inside inference services, CI/CD runners and container images that hold cloud credentials. The root cause is worth more attention than the patch. In every variant the security gate ran in a different place from the code load, and the download itself was two non-atomic HTTP calls rather than one. That pattern is not unique to diffusers, and Zafran said it disclosed a parallel flaw in Hugging Face's transformers package too.
What the three CVEs actually are
DiffusionPipeline.from_pretrained can load a custom pipeline: a Python file inside a model repository containing a class that inherits from DiffusionPipeline. Executing someone else's Python is obviously dangerous, so diffusers gates it behind trust_remote_code=True. Pass anything else and you get a ValueError telling you to inspect the file first.
The gate was implemented inside DiffusionPipeline.download(). The actual code load happened later, in a second call path. Any route that skipped or short-circuited download() therefore skipped the check. The GitHub advisory for CVE-2026-44513 states the root cause plainly: the gate "was implemented inside DiffusionPipeline.download() rather than at the actual dynamic-module load site".
CVE-2026-44827: the None.py string-formatting bug
This is the one that should worry platform teams most, because it needs no unusual arguments at all. When custom_pipeline is not supplied it defaults to None. That None is then interpolated into a filename: f"{None}.py" produces the literal string "None.py". If the repository contains a file called None.py, the resolver treats it as a custom pipeline and loads it.
The None.py advisory shows the attacker payload, which is short enough to fit on a slide:
from diffusers import FluxPipeline as _FluxPipeline
class FluxPipeline(_FluxPipeline):
pass
# malicious code runs here, at import time
import pathlib
pathlib.Path("/tmp/pwned").write_text(":)")
Because the class shadows a real diffusers class, model_index.json can declare "_class_name": "FluxPipeline" and look completely ordinary. The victim's call is the most boring line in machine learning:
from diffusers import DiffusionPipeline
pipeline = DiffusionPipeline.from_pretrained("someone/a-nice-looking-model")
No custom_pipeline kwarg. No trust_remote_code. Nothing odd in the config. The advisory notes None.py is even added to the allow_patterns download filter by the same formatting bug, so it gets fetched automatically on a cold cache. The call then succeeds and returns a working pipeline, which is precisely why nobody notices. Hugging Face's advisory calls it a "silent RCE". Only diffusers 0.37.0 is listed as affected for this CVE.
CVE-2026-44513: three ways past the gate
This CVE covers three variants, all with the same misplaced-gate cause and all affecting versions before 0.37.1:
-
Cross-repository
custom_pipeline.from_pretrained('repoA', custom_pipeline='attacker/repoB', trust_remote_code=False)evaluated the gate againstrepoA's file list, then loaded and ranrepoB'spipeline.py. -
Local snapshot plus Hub
custom_pipeline. Pointing the first argument at a local directory skippeddownload()entirely, so the gate was never reached andrepoB's remote code ran anyway. -
Local snapshot with custom components. A snapshot containing files such as
unet/my_unet_model.py, referenced frommodel_index.json, executed on load because the local path again bypasseddownload().
Variant 3 matters for anyone who thinks air-gapping fixes this. Pre-downloading a snapshot to a shared volume and loading from disk was, before 0.38.0, a less protected path than pulling from the Hub.
CVE-2026-45804: a 0.3-second race
The lowest-scored of the three is the most interesting engineering story. A model download is not one operation. It is an hf_hub_download for the config followed by a snapshot_download for the files. The gate reads the first response. An attacker who edits the repository configuration in the gap between the two requests gets code loaded that the gate never saw.
Zafran measured that window at roughly 0.3 seconds and noted the exploit needs an uncached first download, according to Infosecurity Magazine's report. That sounds like a lottery ticket until you consider volume: on a repository pulled thousands of times a day, briefly pushing a malicious config and reverting it converts a 0.3-second window into a statistical certainty. Every variant traces back to the same class of defect, Time-of-Check to Time-of-Use, because the download was split into two sequential, non-atomic requests instead of one.
The three CVEs side by side
| Vector | CVE | CVSS v3.1 | Affected versions | Trigger the victim must run |
|---|---|---|---|---|
None.py filename formatting |
CVE-2026-44827 | 8.8 (AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H) | 0.37.0 |
from_pretrained('repo') with no kwargs at all |
Cross-repo custom_pipeline
|
CVE-2026-44513 | 8.8 | < 0.37.1 | custom_pipeline='attacker/repoB' |
| Local snapshot + Hub pipeline | CVE-2026-44513 | 8.8 | < 0.37.1 | from_pretrained('/local/snap', custom_pipeline=...) |
| Local snapshot custom components | CVE-2026-44513 | 8.8 | < 0.37.1 |
from_pretrained('/local/snap') on a poisoned snapshot |
| Config swap between two HTTP calls | CVE-2026-45804 | 7.5 | < 0.38.0 | any uncached from_pretrained on a hostile repo |
All five rows are classified CWE-94, improper control of code generation. All five are fixed by the same upgrade.
The fix, and why it is the right fix
Hugging Face shipped diffusers 0.38.0 on 1 May 2026 via PR #13448. The patch moves the trust_remote_code gate out of DiffusionPipeline.download() and into get_cached_module_file in src/diffusers/utils/dynamic_modules_utils.py, which is the chokepoint every dynamic module load passes through, whether the source is the Hub, a local directory or a community mirror. All variants now raise ValueError instead of executing untrusted code.
pip install --upgrade "diffusers>=0.38.0"
That is a one-line change with real security value, because the new gate sits where the exec happens rather than where the download starts. The general lesson: a check that lives one function away from the dangerous operation is a check that a future refactor will bypass.
Zafran reported the first two flaws on 19 March 2026 and the CVEs were published in May, per Infosecurity Magazine. The public research landed on 27 July, twelve weeks after the patch. That is a good disclosure timeline and a bad reason to relax, because most of the exposure window for a library like this is not before the patch but after it, in the long tail of pinned requirements files nobody has revisited.
Find your exposure in an afternoon
Patching is easy. Knowing where diffusers runs is the actual work, because it arrives as a transitive dependency of image-generation services, training notebooks, evaluation harnesses and base container images.
Step 1: enumerate every install
Run this across build agents, GPU nodes and any long-lived notebook host:
# Installed version on the current interpreter
python -c "import diffusers, sys; print(diffusers.__version__, sys.executable)"
# Every environment on the box
find / -name "diffusers" -maxdepth 8 -type d -path "*site-packages*" 2>/dev/null
# Pinned versions across the repo, including transitive pins
grep -rniE "diffusers[=<>~ ]" --include="*.txt" --include="*.toml" \
--include="*.lock" --include="Dockerfile*" .
Container images deserve their own pass. A base image built in March 2026 and never rebuilt still carries 0.37.0 no matter what the application requirements.txt says.
Step 2: hunt for the payload pattern
The None.py variant leaves an unusually clean signature: a Python file at the root of a model repository or snapshot with a name that no legitimate pipeline uses. Scan every cached snapshot before you load it:
# Any .py file inside the HF cache is worth an eyeball
find "${HF_HOME:-$HOME/.cache/huggingface}/hub" -name "*.py" -printf "%p\n"
# The specific bypass filename
find "${HF_HOME:-$HOME/.cache/huggingface}/hub" -name "None.py"
A model snapshot should be tensors, tokenizer files and JSON. Executable Python in unet/, scheduler/ or at the snapshot root is the thing to alert on, which is exactly what Hugging Face's own workaround guidance says: inspect the snapshot for unexpected *.py files, especially under component subdirectories and at the root.
Step 3: check what the process could reach
The CVSS vectors all read C:H/I:H/A:H, so the blast radius is whatever the loading process holds. On a typical inference node that is a cloud instance role, a Hub token with write scope, and network reach into a model registry. Jeremy Powell, CISO at Sumo Logic, told Infosecurity Magazine the defences that mattered here were "the unglamorous ones": egress control, segmentation and credential hygiene, alongside detection operating at the speed of the attack.
Commenting on the separate Hugging Face intrusion reported in July 2026, Crystal Morin, cybersecurity strategist at Sysdig, said what caught it was "behavioral anomaly detection at the infrastructure level" rather than perimeter defences, and advised teams to check they can spot a privileged container spinning up from an application process. Both pieces of advice apply directly: a model load that opens an outbound socket or writes outside the cache is anomalous behaviour you can alert on without knowing the next CVE number.
A model-loading policy that survives the next bypass
Upgrading fixes these three. The policy below is what stops the fourth. Treat model repositories as untrusted code, not as passive data, because configuration files, loaders and custom pipeline code all cross into executable code during a routine load.
Pin and verify, do not float
diffusers>=0.38.0 in a requirements file is a floor, not a guarantee, if anything downstream resolves differently. Use a lockfile with hashes and rebuild base images on a schedule rather than on incident.
Mirror models, do not pull them live
The race-condition CVE only works against a repository you fetch at load time. An internal mirror with a review step between "upstream published" and "production can load" removes that vector regardless of library version. Teams already running an internal registry for weights, for example via model weights as OCI image volumes, have most of this in place.
Scan the artefact, not just the dependency
Hugging Face runs ClamAV malware scanning over every file at each commit, plus pickle scanning and secrets scanning, and integrates third-party scanners. That is useful signal and not a substitute for scanning what you actually cached. The Hub badge tells you about the upstream file at scan time, not about the snapshot sitting on your GPU node.
Prefer formats that cannot execute
Where a model ships both, load safetensors rather than pickle-based checkpoints. Safetensors stores tensor data with no executable code and no deserialization hooks, which removes an entire attack class before you get to library-specific bugs.
Sandbox the first load
Run an unfamiliar model's first load in a container with no cloud credentials, no write access to the registry, and egress restricted to the Hub. If custom code fires, it fires somewhere disposable. This is the same containment argument made for hardening MCP servers against known CVEs and for securing AI agent frameworks after the Langflow RCE: the framework will have another bug, so bound what that bug can touch.
Alert on behaviour, not signatures
Model loading has a narrow, predictable syscall profile. Outbound connections to hosts that are not the Hub, process spawns, and writes outside the cache directory during a from_pretrained call are all cheap detections that generalise past any single CVE.
Build-vs-buy for model supply chain controls
Most teams already own the pieces; the question is whether to assemble them or buy a platform.
| Decision vector | Assemble in-house | Commercial model-security platform |
|---|---|---|
| Time to first coverage | Slower: inventory, scanning and mirror built in sequence | Faster for scanning, similar for policy tuning |
| Upfront cost | Engineering time only | Per-model or per-seat licence |
| Maintenance overhead | Ongoing: new libraries, new formats, new CVEs | Vendor tracks new formats and CVEs |
| Data control | Full: artefacts never leave your network | Depends on scanning architecture |
| Coverage of unknown bugs | Behavioural detection you write yourself | Vendor heuristics plus your own rules |
| Best fit | Teams with an existing internal registry and SOC tooling | Teams loading many third-party models with a small platform team |
The honest answer for most mid-sized teams is a hybrid: buy scanning, build the mirror and the sandbox. The mirror is the control that pays back fastest, because it converts every future model-loading CVE from an emergency into a review-queue item.
India-specific considerations
For Indian teams the exposure is regulatory as well as technical. A remote code execution on an inference host that also processes user data is a personal data breach under the Digital Personal Data Protection Act 2023, with notification obligations attached. The DPDP Rules were notified in November 2025 with full compliance required from May 2027, so the window to get model-loading controls into the security programme is now rather than after an incident. Teams working through that programme will find the sequencing in our DPDP Act engineering playbook for Indian startups.
Two practical notes. First, if you use Hugging Face Enterprise features for storage regions or network security, document that configuration as part of your data-flow mapping. Second, an internal model mirror hosted in-region solves a compliance question and the race-condition CVE at the same time, which makes it easier to fund than a pure security control.
Where this fits in the wider AI supply chain
This is the same story as npm and PyPI, arriving five years later in a different package ecosystem. The dependency you audit is the library; the thing that actually executes is the artefact you download at runtime. Teams that already treat package registries with suspicion, as covered in our note on Dependabot malware alerts across npm and PyPI, usually have not extended the same suspicion to model weights.
The gap is understandable. A model feels like data. It has a size in gigabytes, it has a licence, it has a card describing what it does. But the loading path reads JSON that names classes, resolves those names to files, and imports them. That is a code path, and the FaceHugger variants are what happens when the security check and the import live in different functions.
If you are choosing between hosted APIs and self-hosted open weights, this is a real column in that comparison. Our breakdown of the open-weight self-host decision across Kimi K3, DeepSeek V4 and GLM covers the cost side; supply chain review is the operational cost that rarely makes it into the spreadsheet. The same applies to teams standing up local LLM production stacks on vLLM, Ollama and LM Studio, where model fetching is a routine, unattended step. For a broader view of how the model layer is changing, see our comparison of Gemini 3.5 Pro, GPT-5.6 and Claude Fable 5.
What to do this week
- Inventory every
diffusersinstall including container base images, then upgrade anything below 0.38.0. - Grep cached snapshots for
*.pyfiles, and specifically forNone.py. - Add a CI check that fails a build when a cached model snapshot contains executable Python.
- Confirm your inference nodes cannot reach anything they do not need, and that their credentials are scoped to read.
- Write down who approves a new third-party model. If the answer is "whoever wrote the notebook", that is the finding.
FAQ
Which diffusers versions are vulnerable?
CVE-2026-44827 lists 0.37.0 as affected. CVE-2026-44513 lists all versions below 0.37.1. CVE-2026-45804 covers versions below 0.38.0. Because one upgrade closes every variant, the practical answer is that anything below 0.38.0 should be treated as vulnerable and upgraded without version-by-version analysis.
Does setting trust_remote_code to False protect me?
No, not on affected versions. That is the entire point of the research. All three CVEs execute attacker code while trust_remote_code is False or omitted, because the check ran in DiffusionPipeline.download() rather than at the module load site. Only upgrading to 0.38.0 or later restores the guarantee that flag is supposed to give.
How was this fixed in 0.38.0?
Pull request 13448 moved the trust_remote_code gate out of DiffusionPipeline.download() and into get_cached_module_file in src/diffusers/utils/dynamic_modules_utils.py. That function is the chokepoint for every dynamic module load, whether from the Hub, a local snapshot or a community mirror, so all identified variants now raise a ValueError instead of running code.
Is loading from a local snapshot safer?
Before 0.38.0 it was less safe. Two of the CVE-2026-44513 variants worked specifically because the local-path branch never called download(), so the trust gate was never reached. A poisoned snapshot containing a file such as unet/my_unet_model.py executed on load. Always inspect a snapshot for unexpected Python files.
How large is the exposed population?
The Hacker News reported the package was downloaded more than 8.1 million times in July 2026, citing pepy.tech statistics. Infosecurity Magazine put it at roughly seven million downloads a month, close to 200,000 a day. Either figure describes a library embedded across production pipelines, CI/CD systems and container images rather than a niche dependency.
What is the race condition window in practice?
Zafran's testing put the gap between the configuration fetch and the full repository download at around 0.3 seconds, and the exploit needs an uncached first download. On a heavily pulled repository, briefly publishing a malicious configuration and reverting it turns that narrow window into reliable statistical success across many victims.
Are other Hugging Face libraries affected?
Zafran disclosed a parallel flaw in the transformers package, which it said the Hugging Face security team acknowledged. The underlying design pattern of a trust check separated from the code load is not unique to diffusers, so treat any library that resolves class names from a downloaded configuration as worth reviewing.
What should we monitor if we cannot patch immediately?
Watch for behaviour rather than signatures: outbound network connections during a model load, process spawns from the loading process, and writes outside the cache directory. Restrict egress from inference nodes to the Hub only, scope credentials to read access, and load unfamiliar models first in a disposable container without cloud credentials.
How eCorpIT can help
eCorpIT is a Gurugram-based engineering organisation, founded in 2021, that builds and secures AI platforms for teams running open-weight models in production. We are CMMI Level 5, MSME certified and ISO 27001:2022 certified, and we design AI pipelines aligned with DPDP Act 2023 requirements. Our senior-led teams handle model supply chain inventory, internal model mirrors, snapshot scanning in CI, and the egress and credential controls that limit what a model-loading bug can reach. If you want a review of how third-party models enter your environment, contact us and we will start with an inventory rather than a proposal.
References
- Hugging Face Diffusers Flaws Could Let Model Repositories Execute Arbitrary Code — The Hacker News, 3 August 2026.
- Bugs in Hugging Face Diffusers Bypass Custom Code Safeguard — Infosecurity Magazine, 28 July 2026.
- trust_remote_code bypass via custom_pipeline and local custom components (CVE-2026-44513) — huggingface/diffusers GitHub Security Advisory, 1 May 2026.
- None.py Trust Remote Code Bypass (CVE-2026-44827) — huggingface/diffusers GitHub Security Advisory, 1 May 2026.
- PR #13448: move the trust_remote_code gate to the dynamic module load site — huggingface/diffusers.
- diffusers v0.38.0 release notes — huggingface/diffusers.
- Malware Scanning — Hugging Face Hub documentation.
- Pickle Scanning — Hugging Face Hub documentation.
- Hub Security overview — Hugging Face Hub documentation.
- Safetensors audited as really safe and becoming the default — Hugging Face.
- CWE-94: Improper Control of Generation of Code — MITRE.
- diffusers on PyPI — Python Package Index.
- DPDP Act enforcement dates and compliance timeline — ConsentOS.
Last updated: 4 August 2026.
Top comments (0)