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.
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
What systems are affected?
| Package | Ecosystem | Vulnerable Range | Patched |
|---|---|---|---|
| diffusers | pip | < 0.38.0 | 0.38.0 |
Do you use diffusers? You're affected.
Severity & Risk
Attack Surface
What should I do?
6 steps-
Patch: Upgrade diffusers to >= 0.38.0 which resolves the race condition.
-
Immediate workaround: Pin all from_pretrained() calls with revision='<specific_commit_sha>' — both Hub calls resolve to identical content, eliminating the race window entirely.
-
Offline loading: Use local_files_only=True after a verified baseline download, or load from local directory paths to avoid Hub calls altogether.
-
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.
-
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.
-
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:
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.
Weaknesses (CWE)
CVSS Vector
CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H References
Timeline
Related Vulnerabilities
CVE-2026-44513 8.8 diffusers: trust_remote_code bypass enables silent RCE
Same package: diffusers GHSA-j7w6-vpvq-j3gm 8.8 diffusers: silent RCE via None.py trust_remote_code bypass
Same package: diffusers CVE-2024-2912 10.0 BentoML: RCE via insecure deserialization (CVSS 10)
Same attack type: Supply Chain CVE-2025-5120 10.0 smolagents: sandbox escape enables unauthenticated RCE
Same attack type: Supply Chain CVE-2023-3765 10.0 MLflow: path traversal allows arbitrary file read
Same attack type: Supply Chain