CVE-2026-45804: diffusers: TOCTOU race bypasses trust_remote_code, RCE

GHSA-7wx4-6vff-v64p HIGH
Published May 20, 2026
CISO Take

A time-of-check/time-of-use race condition in HuggingFace diffusers allows an attacker-controlled Hub repository to silently execute arbitrary code during a routine DiffusionPipeline.from_pretrained() call, completely circumventing the trust_remote_code security guard. The attack exploits the ~0.5-second window between two independent Hub API calls: the trust check reads a clean model_index.json (commit A), but snapshot_download immediately after can silently fetch a malicious commit B containing a custom pipeline.py — no warning is shown and the pipeline loads and functions normally. With 393 downstream packages depending on diffusers and the library being central to generative AI workloads, any team running unversioned model loads in CI/CD, automated training, or inference pipelines is exposed; an attacker doesn't need per-victim timing precision against a popular repo, only statistical success across many concurrent downloads. Patch to diffusers >= 0.38.0 immediately, and as a compensating control pin all from_pretrained() calls with revision=<commit_sha> to eliminate the split-fetch window entirely.

Sources: NVD GitHub Advisory ATLAS OpenSSF

What is the risk?

High risk for organizations running automated diffusion model pipelines that load from HuggingFace Hub without pinned commit revisions. CVSS 7.5 reflects AC:H for the timing requirement, but the statistical exploitation model against high-traffic repositories makes this operationally realistic — an attacker cycling repository state every few seconds against a popular model achieves meaningful hit rates at scale without targeting individual victims. The RCE is silent: the pipeline returns fully functional, making detection without process-level monitoring or egress filtering extremely difficult post-exploitation. OpenSSF Scorecard of 4.3/10 signals broader security hygiene concerns in the package. Risk is substantially lower for deployments using pinned revision hashes or loading from local directories, both of which close the race window entirely.

Attack Kill Chain

Repository Staging
Attacker creates or compromises a HuggingFace Hub repository and prepares commit B with a malicious pipeline.py payload and list-format _class_name in model_index.json, while keeping commit A (clean config with plain string _class_name) as the active default.
AML.T0079
Trust Check Bypass
Victim calls DiffusionPipeline.from_pretrained(); hf_hub_download fetches model_index.json from commit A — _class_name is a plain string, load_pipe_from_hub evaluates to False, and the trust_remote_code check passes without raising an error.
AML.T0010.003
Race Window Exploitation
Within the ~0.5-second window between hf_hub_download and snapshot_download, attacker activates commit B; snapshot_download resolves the repository's HEAD to commit B and downloads the malicious pipeline.py to the local HuggingFace cache.
AML.T0010.001
Silent RCE
_get_pipeline_class discovers pipeline.py in the local cache and imports it with no subsequent trust check, executing attacker-controlled code on the victim's ML system with full process privileges while returning a functional pipeline object.
AML.T0011.000

What systems are affected?

Package Ecosystem Vulnerable Range Patched
diffusers pip < 0.38.0 0.38.0
33.6K OpenSSF 4.3 393 dependents Pushed 5d ago 100% patched ~0d to patch Full package profile →

Do you use diffusers? You're affected.

Severity & Risk

CVSS 3.1
7.5 / 10
EPSS
N/A
Exploitation Status
No known exploitation
Sophistication
Moderate

Attack Surface

AV AC PR UI S C I A
AV Network
AC High
PR None
UI Required
S Unchanged
C High
I High
A High

What should I do?

6 steps
  1. Patch: Upgrade diffusers to >= 0.38.0 which resolves the race condition.

  2. Immediate workaround: Pin all from_pretrained() calls with revision='<specific_commit_sha>' — both Hub calls resolve to identical content, eliminating the race window entirely.

  3. Offline loading: Use local_files_only=True after a verified baseline download, or load from local directory paths to avoid Hub calls altogether.

  4. Audit: Search codebases and CI/CD configs for DiffusionPipeline.from_pretrained() calls missing a revision= parameter; treat any dynamic Hub load as high-risk until patched.

  5. Detection: Monitor for unexpected Python imports or outbound network connections spawned during model loading; alert on pipeline.py or similarly named files appearing in HuggingFace cache directories (~/.cache/huggingface/) post-download.

  6. Supply chain hygiene: Enforce a policy of loading models only from organization-controlled forks with locked revisions for production workloads.

Classification

Compliance Impact

This CVE is relevant to:

EU AI Act
Art. 9 - Risk management system
ISO 42001
A.6.2.3 - AI supply chain management
NIST AI RMF
GOVERN 1.2 - Accountability for AI risk
OWASP LLM Top 10
LLM03 - Supply Chain Vulnerabilities

Frequently Asked Questions

What is CVE-2026-45804?

A time-of-check/time-of-use race condition in HuggingFace diffusers allows an attacker-controlled Hub repository to silently execute arbitrary code during a routine DiffusionPipeline.from_pretrained() call, completely circumventing the trust_remote_code security guard. The attack exploits the ~0.5-second window between two independent Hub API calls: the trust check reads a clean model_index.json (commit A), but snapshot_download immediately after can silently fetch a malicious commit B containing a custom pipeline.py — no warning is shown and the pipeline loads and functions normally. With 393 downstream packages depending on diffusers and the library being central to generative AI workloads, any team running unversioned model loads in CI/CD, automated training, or inference pipelines is exposed; an attacker doesn't need per-victim timing precision against a popular repo, only statistical success across many concurrent downloads. Patch to diffusers >= 0.38.0 immediately, and as a compensating control pin all from_pretrained() calls with revision=<commit_sha> to eliminate the split-fetch window entirely.

Is CVE-2026-45804 actively exploited?

No confirmed active exploitation of CVE-2026-45804 has been reported, but organizations should still patch proactively.

How to fix CVE-2026-45804?

1. Patch: Upgrade diffusers to >= 0.38.0 which resolves the race condition. 2. Immediate workaround: Pin all from_pretrained() calls with revision='<specific_commit_sha>' — both Hub calls resolve to identical content, eliminating the race window entirely. 3. Offline loading: Use local_files_only=True after a verified baseline download, or load from local directory paths to avoid Hub calls altogether. 4. Audit: Search codebases and CI/CD configs for DiffusionPipeline.from_pretrained() calls missing a revision= parameter; treat any dynamic Hub load as high-risk until patched. 5. Detection: Monitor for unexpected Python imports or outbound network connections spawned during model loading; alert on pipeline.py or similarly named files appearing in HuggingFace cache directories (~/.cache/huggingface/) post-download. 6. Supply chain hygiene: Enforce a policy of loading models only from organization-controlled forks with locked revisions for production workloads.

What systems are affected by CVE-2026-45804?

This vulnerability affects the following AI/ML architecture patterns: Diffusion model inference pipelines, Generative AI training pipelines, MLOps CI/CD automation, Model evaluation and benchmarking workflows, HuggingFace Hub-integrated services.

What is the CVSS score for CVE-2026-45804?

CVE-2026-45804 has a CVSS v3.1 base score of 7.5 (HIGH).

Technical Details

NVD Description

## Background This vulnerability is found in the `diffusers` package - the `transformers`-equivalent library for diffusion models. It is found in the `DiffusionPipeline.from_pretrained` flow, which is used to load a pipeline from the HuggingFace Hub. This function has a `trust_remote_code` guard: if the repository’s `model_index.json` references a custom pipeline class defined in a `.py` file in the repo, the load is blocked unless `trust_remote_code=True` is explicitly passed: ``` ValueError: The repository for attacker/repo contains custom code in pipeline.py which must be executed to correctly load the model. You can inspect the repository content at https://hf.co/attacker/repo/blob/main/pipeline.py. Please pass the argument `trust_remote_code=True` to allow custom code to be run. ``` The vulnerability allows arbitrary code execution through the custom pipeline flow from a Hub repo, with no `custom_pipeline` or `trust_remote_code` kwargs passed. The `from_pretrained` call succeeds and returns a functional pipeline. --- ## Naive Flow `DiffusionPipeline.from_pretrained` begins by popping all relevant arguments from `kwargs` into local variables, then calls `DiffusionPipeline.download()` to fetch the repo files: ```python # pipeline_utils.py:853 cached_folder = cls.download( pretrained_model_name_or_path, ... custom_pipeline=custom_pipeline, trust_remote_code=trust_remote_code, ... ) ``` Inside `download()`, `model_index.json` is fetched first as a standalone file via `hf_hub_download`: ```python # pipeline_utils.py:1636 config_file = hf_hub_download( pretrained_model_name, cls.config_name, ... ) config_dict = cls._dict_from_json_file(config_file) ``` This config is used to detect custom pipeline code and enforce the trust check: ```python # pipeline_utils.py:1672 if custom_pipeline is None and isinstance(config_dict["_class_name"], (list, tuple)): custom_pipeline = config_dict["_class_name"][0] load_pipe_from_hub = custom_pipeline is not None and f"{custom_pipeline}.py" in filenames if load_pipe_from_hub and not trust_remote_code: raise ValueError(...) ``` After the check passes, `snapshot_download` then fetches all files and saves them to disk: ```python # pipeline_utils.py:1778 cached_folder = snapshot_download( pretrained_model_name, ... revision=revision, allow_patterns=allow_patterns, ... ) ``` Back in `from_pretrained`, the config is read a second time from the downloaded snapshot, and`_resolve_custom_pipeline_and_cls` reads the config to re-check if custom code needs to be loaded: ```python # pipeline_loading_utils.py:974 def _resolve_custom_pipeline_and_cls(folder, config, custom_pipeline): custom_class_name = None if os.path.isfile(os.path.join(folder, f"{custom_pipeline}.py")): custom_pipeline = os.path.join(folder, f"{custom_pipeline}.py") elif isinstance(config["_class_name"], (list, tuple)) and os.path.isfile( os.path.join(folder, f"{config['_class_name'][0]}.py") ): custom_pipeline = os.path.join(folder, f"{config['_class_name'][0]}.py") custom_class_name = config["_class_name"][1] return custom_pipeline, custom_class_name ``` If the config points to a `.py` file, it is imported. --- ## The Vulnerability `hf_hub_download` and `snapshot_download` are two independent HTTP calls to the Hub, both resolving the repository’s default branch (if `revision=None`) to its current HEAD at call time. There is no atomicity guarantee between them - if the repository is updated between the two calls, they will resolve to different commits and download different content, with no warning displayed to the user. The trust check in `download()` operates on the content fetched by `hf_hub_download` (commit A). The `snapshot_download` call that immediately follows can silently fetch a newer commit (commit B). The config in the newer commit will be the one parsed by `_resolve_custom_pipeline_and_cls`. **Therefore, it’s possible to introduce remote code into the repo between the two calls, bypassing the trust check.** The race window is everything between the two Hub calls inside `download()`: ```python # pipeline_utils.py:1636 config_file = hf_hub_download(...) # ← sees commit A, trust check passes # ... filenames processing, pattern building, pipeline_is_cached check ... # ~~~ ATTACKER PUSHES COMMIT B HERE ~~~ # pipeline_utils.py:1778 cached_folder = snapshot_download(...) # ← sees commit B, downloads pipeline.py ``` For the exploit, commit A carries a clean config with `_class_name` as a plain string, which causes `load_pipe_from_hub` to be `False` and the trust check to pass. Commit B changes `_class_name` to a list and adds `pipeline.py`: **Commit A - `model_index.json`:** ```json { "_class_name": "FluxPipeline", "_diffusers_version": "0.31.0" } ``` **Commit B - `model_index.json`:** ```json { "_class_name": ["pipeline", "FluxPipeline"], "_diffusers_version": "0.31.0" } ``` When `from_pretrained` reads the snapshot after `download()` returns, `config["_class_name"]` is now a list, `pipeline.py` exists on disk (fetched by `snapshot_download`), and `_resolve_custom_pipeline_and_cls` resolves `custom_pipeline` to the local path of that file. `_get_pipeline_class` then imports it - with no trust check at this point in the code. --- ## PoC 1. Create a Hub repo with commit A’s `model_index.json` (plain string `_class_name`). 2. Run `DiffusionPipeline.from_pretrained("attacker/repo")` with a breakpoint set at `pipeline_utils.py:1778` (the `snapshot_download` call). This is for the window to be large enough to manually respond to it. 3. When execution pauses at the breakpoint, push commit B: update `model_index.json` to use a list `_class_name` and add `pipeline.py`. 4. Resume execution. 5. `snapshot_download` fetches commit B; `/tmp/pwned` is written during the subsequent `_get_pipeline_class` call. --- ## Constraints - Does not apply when `revision` is pinned to a specific commit hash - both Hub calls resolve to the same content. - Does not apply when loading from a local directory. - If all expected files are already present in the local HF cache, `download()` returns early before reaching `snapshot_download` (line 1767 early-return), closing the race window. The exploit therefore requires a first (or forced) download. --- ## Exploitability The window between the two calls is very short. Local testing resulted in a window of approximately ~0.5 seconds for the attacker to push the change. This is, of course, unfeasible to accomplish for each and every new download. However, given a popular repo with many downloads per day, one may achieve **statistical success** by changing the repo’s state every once in a while or every few seconds, with some percentage of downloaders falling on the exact window. --- ## Impact The vulnerability is a silent RCE - it allows arbitrary code to be loaded through the custom pipeline flow from a Hub repo, with no `custom_pipeline` or `trust_remote_code` kwargs. The `from_pretrained` call succeeds and returns a fully functional pipeline.

Exploitation Scenario

An adversary creates a convincing HuggingFace repository hosting a popular diffusion model (or compromises an existing one with significant traffic). They maintain commit A with a clean model_index.json where _class_name is a plain string — this ensures the trust check passes for all incoming downloads. They deploy automation that rapidly cycles the repository between commit A and a malicious commit B every few seconds: commit B changes _class_name to a list and adds a pipeline.py containing a reverse shell payload or credential harvester targeting cloud provider metadata endpoints. Against a repo receiving thousands of daily downloads, a statistically significant fraction of from_pretrained() calls hit the split-commit window: hf_hub_download resolves commit A (trust check passes), then snapshot_download resolves commit B (malicious pipeline.py downloaded). Victims' ML workers silently execute the payload — with no error, no warning, and a fully functional pipeline returned — giving the attacker persistent access to GPU clusters, training datasets, and cloud credentials before any detection occurs.

CVSS Vector

CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H

Timeline

Published
May 20, 2026
Last Modified
May 20, 2026
First Seen
May 20, 2026

Related Vulnerabilities