Impact
A trustremotecode bypass in DiffusionPipeline.frompretrained allows arbitrary remote code execution despite the user passing trustremotecode=False (or omitting it, which is the default). The vulnerability has three variants, all sharing the same root cause — the trustremotecode gate was implemented inside DiffusionPipeline.download() rather than at the actual dynamic-module load site, so any code path that bypassed or short-circuited download() also bypassed the security check:
1. Cross-repo custompipeline. DiffusionPipeline.frompretrained('repoA', custompipeline='attacker/repoB', trustremotecode=False) — the gate evaluated against repoA's file list rather than repoB's, so repoB's pipeline.py was loaded and executed. 2. Local snapshot + Hub custompipeline. DiffusionPipeline.frompretrained('/local/snapshot', custompipeline='attacker/repoB', trustremotecode=False) — the local-path branch never invoked download(), so the gate was never reached and remote code from repoB executed. 3. Local snapshot with custom components. DiffusionPipeline.frompretrained('/local/snapshot', trustremotecode=False) where the snapshot contains custom component files (e.g. unet/myunetmodel.py) referenced from modelindex.json — same root cause; the local path skipped download() and custom component code executed.
Silent remote code execution on the victim's machine. Anyone calling DiffusionPipeline.frompretrained with custom pipelines is impacted.
Patches
Yes. Fixed in diffusers 0.38.0 via PR #13448. All users on versions < 0.38.0 should upgrade:
bash pip install --upgrade "diffusers>=0.38.0"
The fix moves the trustremotecode gate out of DiffusionPipeline.download() and into getcachedmodulefile in src/diffusers/utils/dynamicmodulesutils.py, which is the actual chokepoint for every dynamic module load (local, Hub, or community mirror). All three variants now raise ValueError instead of executing untrusted code.
Workarounds
If upgrading immediately is not possible:
- Only call frompretrained with pretrainedmodelnameorpath, custompipeline, and local snapshot directories from fully trusted sources that have been audited. - Do not pass custompipeline= pointing at a Hub repository different from the primary pretrainedmodelnameorpath before reading its pipeline.py. - Before calling frompretrained on a local snapshot, inspect the snapshot for unexpected .py files, especially under component subdirectories (unet/, scheduler/, etc.) and at the snapshot root.
These are mitigations, not fixes — the only complete remediation is upgrading to 0.38.0.
Resources
- Fix: https://github.com/huggingface/diffusers/pull/13448 - Original issue: https://github.com/huggingface/diffusers/issues/13446 - Release notes: https://github.com/huggingface/diffusers/releases/tag/v0.38.0 - CWE-94: https://cwe.mitre.org/data/definitions/94.html
Background
This vulnerability is found in the DiffusionPipeline.frompretrained flow, which is used to load a pipeline from the HuggingFace Hub.
This function accepts an optional custompipeline keyword argument: the name of a Python file in the repo that contains a custom class inheriting from DiffusionPipeline. An equivalent flow is triggered when the classname field in modelindex.json (the repo config file) is set to a custom class.
Any attempt to use a custom pipeline throws the following exception, requesting that trustremotecode is also passed:
python DiffusionPipeline.frompretrained( pretrainedmodelnameorpath='ido-shani/custom-pipeline', custompipeline="custom" )
ValueError: The repository for ido-shani/custom-pipeline contains custom code in custom.py which must be executed to correctly load the model. You can inspect the repository content at https://hf.co/ido-shani/custom-pipeline/blob/main/custom.py. Please pass the argument trustremotecode=True to allow custom code to be run.
The vulnerability is a silent RCE - it allows arbitrary code to be loaded through the custom\pipeline flow from a Hub repo, with no custompipeline or trustremotecode kwargs and nothing suspicious in the config. The frompretrained call succeeds and returns a functional pipeline.
Naive Flow
First, all relevant arguments are popped from kwargs and stored in local variables.
Given a pretrainedmodelnameorpath that is a Hub repo ID, DiffusionPipeline.download() is called. This function serves two roles: it orchestrates downloading relevant model files, and it is the security gatekeeper for trustremotecode. It is called even if the model is already cached; in that case it exits early. If the repo contains custom code, it checks whether trustremotecode was passed and raises otherwise:
python pipelineutils.py:1645-1652 loadpipefromhub = custompipeline is not None and f"{custompipeline}.py" in filenames
...
if loadpipefromhub and not trustremotecode: raise ValueError(...)
It then runs getpipelineclass, which returns the class object of the pipeline in order to inspect its init signature and determine which component files need to be downloaded. As part of building the allowpatterns list used to filter the snapshot download to necessary files only, the custom pipeline file is explicitly included if present:
python pipelineutils.py:1707 allowpatterns += [f"{custompipeline}.py"] if f"{custompipeline}.py" in filenames else []
The function then checks if all expected files are already present, and either exits early or triggers a snapshot download with those patterns.
The next step in frompretrained is loading the pipeline class a second time, this time to actually instantiate it. Before calling getpipelineclass again, resolvecustompipelineandcls is called to translate the custompipeline name into a local path, since the files have already been downloaded:
python pipelineloadingutils.py:965-974 def resolvecustompipelineandcls(folder, config, custompipeline): customclassname = None if os.path.isfile(os.path.join(folder, f"{custompipeline}.py")): custompipeline = os.path.join(folder, f"{custompipeline}.py") elif isinstance(config["classname"], (list, tuple)) and os.path.isfile( os.path.join(folder, f"{config['classname'][0]}.py") ): custompipeline = os.path.join(folder, f"{config['classname'][0]}.py") customclassname = config["classname"][1]
return custompipeline, customclassname
When customclassname is None (i.e. custompipeline was given as a kwarg rather than via the config), getpipelineclass will scan the file and automatically identify the class that subclasses DiffusionPipeline.
Once this is done, getpipelineclass is invoked with the resolved local path, which loads the custom code, retrieves the class object, and proceeds with instantiation.
The Vulnerability
resolvecustompipelineandcls receives custompipeline from the kwargs - when not supplied it defaults to None. That None is used in string formatting: f"{None}.py" = "None.py".
If the repo contains a file with this name, it will be detected as a custom pipeline.
This is only reached on the second invocation of getpipelineclass (inside frompretrained, after download() returns). The trust\remote\code check lives entirely in download(), which evaluated custompipeline is None -> False and skipped it. By the time resolvecustompipelineandcls runs, it is no longer relevant.
As a bonus, None.py even gets downloaded automatically when the model isn't cached yet. This isn't strictly required - it is quite plausible that the victim has already run hf download <model> and has all files locally - but if they haven't, revisiting the allowpatterns line above shows it makes the same error: f"{None}.py" = "None.py" is added to allowpatterns and fetched.
What should None.py contain? To avoid breaking the pipeline load, it must define a class inheriting from DiffusionPipeline. To avoid leaving suspicious clues in the config, that class should shadow one that already exists in diffusers. The following satisfies both requirements:
python from diffusers import FluxPipeline as FluxPipeline
class FluxPipeline(FluxPipeline): pass
INSERT MALICIOUS CODE HERE import pathlib pathlib.Path("/tmp/pwned").writetext(":)")
With this, modelindex.json can contain "classname": "FluxPipeline" - appearing to use the standard diffusers class - and the resulting pipeline is fully functional (it is also functional when running as a local directory). This has been verified against an extracted version of DDUF/tiny-flux-dev-pipe-dduf.
All the attacker needs the victim to run is:
python from diffusers import DiffusionPipeline
pipeline = DiffusionPipeline.frompretrained('ido-shani/none-py-trust-remote-code-bypass')
PoC
- Upload this zip as a model to the hub. https://drive.google.com/file/d/1mULARMLJJUTCi57xIv0wtDauko-JW0h7/view?usp=sharing - Run DiffusionPipeline.frompretrained on the uploaded model hub identifier. - RCE occured; /tmp/pwned was created. If you are running the exploit on windows, change the path touched in None.py.
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 custompipeline or trustremotecode kwargs and nothing suspicious in the config. The frompretrained call succeeds and returns a functional pipeline.
Occurrences
https://github.com/huggingface/diffusers/blob/e1b5db52bda85d47a4f8f75954f77e672a8f7f1c/src/diffusers/pipelines/pipelineloadingutils.py#L976
Patches
Yes. Fixed in diffusers 0.38.0 via PR #13448. All users on versions < 0.38.0 should upgrade:
bash pip install --upgrade "diffusers>=0.38.0"
The fix moves the trustremotecode gate out of DiffusionPipeline.download() and into getcachedmodulefile in src/diffusers/utils/dynamicmodulesutils.py, which is the actual chokepoint for every dynamic module load (local, Hub, or community mirror). All three variants now raise ValueError when trustremotecode=False instead of executing untrusted code.
Workarounds
If upgrading immediately is not possible:
- Only call frompretrained with pretrainedmodelnameorpath, custompipeline, and local snapshot directories from sources you fully trust and have audited. - Do not pass custompipeline= pointing at a Hub repository different from the primary pretrainedmodelnameorpath unless you have read its pipeline.py. - Before calling frompretrained on a local snapshot, inspect the snapshot for unexpected .py files, especially under component subdirectories (unet/, scheduler/, etc.) and at the snapshot root.
Why this should have a dedicated CVE
GHSA-j7w6-vpvq-j3gm is a distinct defect from CVE-2026-44513. CVE-2026-44513 is a misplaced-security-gate bug requiring a user-supplied custompipeline argument or a config entry declaring custom code. GHSA-j7w6 is a string-formatting bug where the default custompipeline=None is interpolated into the filename None.py, allowing silent RCE on a fully default frompretrained('repo') call with no kwargs and a modelindex.json that shadows a legitimate class. The root cause root cause and trigger are different, although the fix applied to address CVE-2026-44513 also addresses this vulnerability.
Background
This vulnerability is found in the diffusers package - the transformers-equivalent library for diffusion models.
It is found in the DiffusionPipeline.frompretrained flow, which is used to load a pipeline from the HuggingFace Hub.
This function has a trustremotecode guard: if the repository’s modelindex.json references a custom pipeline class defined in a .py file in the repo, the load is blocked unless trustremotecode=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 trustremotecode=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 custompipeline or trustremotecode kwargs passed. The frompretrained call succeeds and returns a functional pipeline.
---
Naive Flow
DiffusionPipeline.frompretrained begins by popping all relevant arguments from kwargs into local variables, then calls DiffusionPipeline.download() to fetch the repo files:
python pipelineutils.py:853 cachedfolder = cls.download( pretrainedmodelnameorpath, ... custompipeline=custompipeline, trustremotecode=trustremotecode, ... )
Inside download(), modelindex.json is fetched first as a standalone file via hfhubdownload:
python pipelineutils.py:1636 configfile = hfhubdownload( pretrainedmodelname, cls.configname, ... ) configdict = cls.dictfromjsonfile(configfile)
This config is used to detect custom pipeline code and enforce the trust check:
python pipelineutils.py:1672 if custompipeline is None and isinstance(configdict["classname"], (list, tuple)): custompipeline = configdict["classname"][0]
loadpipefromhub = custompipeline is not None and f"{custompipeline}.py" in filenames
if loadpipefromhub and not trustremotecode: raise ValueError(...)
After the check passes, snapshotdownload then fetches all files and saves them to disk:
python pipelineutils.py:1778 cachedfolder = snapshotdownload( pretrainedmodelname, ... revision=revision, allowpatterns=allowpatterns, ... )
Back in frompretrained, the config is read a second time from the downloaded snapshot, andresolvecustompipelineandcls reads the config to re-check if custom code needs to be loaded:
python pipelineloadingutils.py:974 def resolvecustompipelineandcls(folder, config, custompipeline): customclassname = None if os.path.isfile(os.path.join(folder, f"{custompipeline}.py")): custompipeline = os.path.join(folder, f"{custompipeline}.py") elif isinstance(config["classname"], (list, tuple)) and os.path.isfile( os.path.join(folder, f"{config['classname'][0]}.py") ): custompipeline = os.path.join(folder, f"{config['classname'][0]}.py") customclassname = config["classname"][1]
return custompipeline, customclassname
If the config points to a .py file, it is imported.
---
The Vulnerability
hfhubdownload and snapshotdownload 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 hfhubdownload (commit A). The snapshotdownload call that immediately follows can silently fetch a newer commit (commit B). The config in the newer commit will be the one parsed by resolvecustompipelineandcls.
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 pipelineutils.py:1636 configfile = hfhubdownload(...) # ← sees commit A, trust check passes
... filenames processing, pattern building, pipelineiscached check ... ~~~ ATTACKER PUSHES COMMIT B HERE ~~~
pipelineutils.py:1778 cachedfolder = snapshotdownload(...) # ← sees commit B, downloads pipeline.py
For the exploit, commit A carries a clean config with classname as a plain string, which causes loadpipefromhub to be False and the trust check to pass. Commit B changes classname to a list and adds pipeline.py:
Commit A - modelindex.json:
json { "classname": "FluxPipeline", "diffusersversion": "0.31.0" }
Commit B - modelindex.json:
json { "classname": ["pipeline", "FluxPipeline"], "diffusersversion": "0.31.0" }
When frompretrained reads the snapshot after download() returns, config["classname"] is now a list, pipeline.py exists on disk (fetched by snapshotdownload), and resolvecustompipelineandcls resolves custompipeline to the local path of that file. getpipelineclass then imports it - with no trust check at this point in the code.
---
PoC
1. Create a Hub repo with commit A’s modelindex.json (plain string classname). 2. Run DiffusionPipeline.frompretrained("attacker/repo") with a breakpoint set at pipelineutils.py:1778 (the snapshotdownload 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 modelindex.json to use a list classname and add pipeline.py. 4. Resume execution. 5. snapshotdownload fetches commit B; /tmp/pwned is written during the subsequent getpipelineclass 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 snapshotdownload (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 custompipeline or trustremotecode kwargs. The frompretrained call succeeds and returns a fully functional pipeline.