GHSA-J7W6-VPVQ-J3GM

Vulnerability from github – Published: 2026-05-07 02:24 – Updated: 2026-05-14 20:53
VLAI?
Summary
Diffusers has a `trust_remote_code` bypass via `custom_pipeline` and local custom components
Details

Background

This vulnerability is found in the DiffusionPipeline.from_pretrained flow, which is used to load a pipeline from the HuggingFace Hub.

This function accepts an optional custom_pipeline 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 _class_name field in model_index.json (the repo config file) is set to a custom class.

Any attempt to use a custom pipeline throws the following exception, requesting that trust_remote_code is also passed:

DiffusionPipeline.from_pretrained(
    pretrained_model_name_or_path='ido-shani/custom-pipeline',
    custom_pipeline="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 `trust_remote_code=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 custom_pipeline or trust_remote_code kwargs and nothing suspicious in the config. The from_pretrained call succeeds and returns a functional pipeline.

Naive Flow

First, all relevant arguments are popped from kwargs and stored in local variables.

Given a pretrained_model_name_or_path 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 trust_remote_code. 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 trust_remote_code was passed and raises otherwise:

# pipeline_utils.py:1645-1652
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(...)

It then runs _get_pipeline_class, 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 allow_patterns list used to filter the snapshot download to necessary files only, the custom pipeline file is explicitly included if present:

# pipeline_utils.py:1707
allow_patterns += [f"{custom_pipeline}.py"] if f"{custom_pipeline}.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 from_pretrained is loading the pipeline class a second time, this time to actually instantiate it. Before calling _get_pipeline_class again, _resolve_custom_pipeline_and_cls is called to translate the custom_pipeline name into a local path, since the files have already been downloaded:

# pipeline_loading_utils.py:965-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

When custom_class_name is None (i.e. custom_pipeline was given as a kwarg rather than via the config), _get_pipeline_class will scan the file and automatically identify the class that subclasses DiffusionPipeline.

Once this is done, _get_pipeline_class is invoked with the resolved local path, which loads the custom code, retrieves the class object, and proceeds with instantiation.

The Vulnerability

_resolve_custom_pipeline_and_cls receives custom_pipeline 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 _get_pipeline_class (inside from_pretrained, after download() returns). The trust_remote_code check lives entirely in download(), which evaluated custom_pipeline is None -> False and skipped it. By the time _resolve_custom_pipeline_and_cls 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 allow_patterns line above shows it makes the same error: f"{None}.py" = "None.py" is added to allow_patterns 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:

from diffusers import FluxPipeline as _FluxPipeline

class FluxPipeline(_FluxPipeline):
    pass

# INSERT MALICIOUS CODE HERE
import pathlib
pathlib.Path("/tmp/pwned").write_text(":)")

With this, model_index.json can contain "_class_name": "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:

from diffusers import DiffusionPipeline

pipeline = DiffusionPipeline.from_pretrained('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.from_pretrained 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 custom_pipeline or trust_remote_code kwargs and nothing suspicious in the config. The from_pretrained call succeeds and returns a functional pipeline.

Occurrences

https://github.com/huggingface/diffusers/blob/e1b5db52bda85d47a4f8f75954f77e672a8f7f1c/src/diffusers/pipelines/pipeline_loading_utils.py#L976

Patches

Yes. Fixed in diffusers 0.38.0 via PR #13448. All users on versions < 0.38.0 should upgrade:

pip install --upgrade "diffusers>=0.38.0"

The fix 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 actual chokepoint for every dynamic module load (local, Hub, or community mirror). All three variants now raise ValueError when trust_remote_code=False instead of executing untrusted code.

Workarounds

If upgrading immediately is not possible:

  • Only call from_pretrained with pretrained_model_name_or_path, custom_pipeline, and local snapshot directories from sources you fully trust and have audited.
  • Do not pass custom_pipeline= pointing at a Hub repository different from the primary pretrained_model_name_or_path unless you have read its pipeline.py.
  • Before calling from_pretrained 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 custom_pipeline argument or a config entry declaring custom code. GHSA-j7w6 is a string-formatting bug where the default custom_pipeline=None is interpolated into the filename None.py, allowing silent RCE on a fully default from_pretrained('repo') call with no kwargs and a model_index.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.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "PyPI",
        "name": "diffusers"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "0.38.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-44827"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-94"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-05-07T02:24:22Z",
    "nvd_published_at": "2026-05-14T17:16:23Z",
    "severity": "HIGH"
  },
  "details": "## Background\n\nThis vulnerability is found in the `DiffusionPipeline.from_pretrained` flow, which is used to load a pipeline from the HuggingFace Hub.\n\nThis function accepts an optional `custom_pipeline` 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 `_class_name` field in `model_index.json` (the repo config file) is set to a custom class.\n\nAny attempt to use a custom pipeline throws the following exception, requesting that `trust_remote_code` is also passed:\n\n```python\nDiffusionPipeline.from_pretrained(\n    pretrained_model_name_or_path=\u0027ido-shani/custom-pipeline\u0027,\n    custom_pipeline=\"custom\"\n)\n\nValueError: The repository for ido-shani/custom-pipeline contains custom code in\ncustom.py which must be executed to correctly load the model. You can inspect the\nrepository content at https://hf.co/ido-shani/custom-pipeline/blob/main/custom.py.\nPlease pass the argument `trust_remote_code=True` to allow custom code to be run.\n```\n\nThe 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 and nothing suspicious in the config. The `from_pretrained` call succeeds and returns a functional pipeline.\n\n## Naive Flow\n\nFirst, all relevant arguments are popped from kwargs and stored in local variables.\n\nGiven a `pretrained_model_name_or_path` 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 `trust_remote_code`. 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 `trust_remote_code` was passed and raises otherwise:\n\n```python\n# pipeline_utils.py:1645-1652\nload_pipe_from_hub = custom_pipeline is not None and f\"{custom_pipeline}.py\" in filenames\n\n...\n\nif load_pipe_from_hub and not trust_remote_code:\n    raise ValueError(...)\n```\n\nIt then runs `_get_pipeline_class`, 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 `allow_patterns` list used to filter the snapshot download to necessary files only, the custom pipeline file is explicitly included if present:\n\n```python\n# pipeline_utils.py:1707\nallow_patterns += [f\"{custom_pipeline}.py\"] if f\"{custom_pipeline}.py\" in filenames else []\n```\n\nThe function then checks if all expected files are already present, and either exits early or triggers a snapshot download with those patterns.\n\nThe next step in `from_pretrained` is loading the pipeline class a second time, this time to actually instantiate it. Before calling `_get_pipeline_class` again, `_resolve_custom_pipeline_and_cls` is called to translate the `custom_pipeline` name into a local path, since the files have already been downloaded:\n\n```python\n# pipeline_loading_utils.py:965-974\ndef _resolve_custom_pipeline_and_cls(folder, config, custom_pipeline):\n    custom_class_name = None\n    if os.path.isfile(os.path.join(folder, f\"{custom_pipeline}.py\")):\n        custom_pipeline = os.path.join(folder, f\"{custom_pipeline}.py\")\n    elif isinstance(config[\"_class_name\"], (list, tuple)) and os.path.isfile(\n        os.path.join(folder, f\"{config[\u0027_class_name\u0027][0]}.py\")\n    ):\n        custom_pipeline = os.path.join(folder, f\"{config[\u0027_class_name\u0027][0]}.py\")\n        custom_class_name = config[\"_class_name\"][1]\n\n    return custom_pipeline, custom_class_name\n```\n\nWhen `custom_class_name` is `None` (i.e. `custom_pipeline` was given as a kwarg rather than via the config), `_get_pipeline_class` will scan the file and automatically identify the class that subclasses `DiffusionPipeline`.\n\nOnce this is done, `_get_pipeline_class` is invoked with the resolved local path, which loads the custom code, retrieves the class object, and proceeds with instantiation.\n\n## The Vulnerability\n\n`_resolve_custom_pipeline_and_cls` receives `custom_pipeline` from the kwargs - when not supplied it defaults to `None`. That `None` is used in string formatting: `f\"{None}.py\"` = `\"None.py\"`.\n\n**If the repo contains a file with this name, it will be detected as a custom pipeline.**\n\nThis is only reached on the second invocation of `_get_pipeline_class` (inside `from_pretrained`, after `download()` returns). The trust\\_remote\\_code check lives entirely in `download()`, which evaluated `custom_pipeline is None -\u003e False` and skipped it. By the time `_resolve_custom_pipeline_and_cls` runs, it is no longer relevant.\n\nAs a bonus, `None.py` even gets downloaded automatically when the model isn\u0027t cached yet. This isn\u0027t strictly required - it is quite plausible that the victim has already run `hf download \u003cmodel\u003e` and has all files locally - but if they haven\u0027t, revisiting the `allow_patterns` line above shows it makes the same error: `f\"{None}.py\"` = `\"None.py\"` is added to `allow_patterns` and fetched.\n\nWhat 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:\n\n```python\nfrom diffusers import FluxPipeline as _FluxPipeline\n\nclass FluxPipeline(_FluxPipeline):\n    pass\n\n# INSERT MALICIOUS CODE HERE\nimport pathlib\npathlib.Path(\"/tmp/pwned\").write_text(\":)\")\n```\n\nWith this, `model_index.json` can contain `\"_class_name\": \"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](https://huggingface.co/DDUF/tiny-flux-dev-pipe-dduf).\n\nAll the attacker needs the victim to run is:\n\n```python\nfrom diffusers import DiffusionPipeline\n\npipeline = DiffusionPipeline.from_pretrained(\u0027ido-shani/none-py-trust-remote-code-bypass\u0027)\n```\n\n## PoC\n\n-   Upload this zip as a model to the hub. https://drive.google.com/file/d/1mULARMLJJUTCi57xIv0wtDauko-JW0h7/view?usp=sharing\n-   Run `DiffusionPipeline.from_pretrained` on the uploaded model hub identifier.\n-   RCE occured; `/tmp/pwned` was created. If you are running the exploit on windows, change the path touched in `None.py`.\n\n# Impact\n\nThe 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 and nothing suspicious in the config. The `from_pretrained` call succeeds and returns a functional pipeline.\n\n# Occurrences\n\nhttps://github.com/huggingface/diffusers/blob/e1b5db52bda85d47a4f8f75954f77e672a8f7f1c/src/diffusers/pipelines/pipeline_loading_utils.py#L976\n\n# Patches\n\nYes. Fixed in **diffusers 0.38.0** via [PR #13448](https://github.com/huggingface/diffusers/pull/13448). All users on versions `\u003c 0.38.0` should upgrade:\n\n```bash\npip install --upgrade \"diffusers\u003e=0.38.0\"\n```\n\nThe fix 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 actual chokepoint for every dynamic module load (local, Hub, or community mirror). All three variants now raise `ValueError` when trust_remote_code=False instead of executing untrusted code.  \n\n# Workarounds\n\nIf upgrading immediately is not possible:\n\n- Only call `from_pretrained` with `pretrained_model_name_or_path`, `custom_pipeline`, and local snapshot directories from sources you fully trust and have audited.\n- Do not pass `custom_pipeline=` pointing at a Hub repository different from the primary `pretrained_model_name_or_path` unless you have read its `pipeline.py`.\n- Before calling `from_pretrained` on a local snapshot, inspect the snapshot for unexpected `*.py` files, especially under component subdirectories (`unet/`, `scheduler/`, etc.) and at the snapshot root.\n\n# Why this should have a dedicated CVE\n\nGHSA-j7w6-vpvq-j3gm is a distinct defect from CVE-2026-44513. CVE-2026-44513 is a misplaced-security-gate bug requiring a user-supplied `custom_pipeline` argument or a config entry declaring custom code. GHSA-j7w6 is a string-formatting bug where the default custom_pipeline=None is interpolated into the filename `None.py`, allowing silent RCE on a fully default `from_pretrained(\u0027repo\u0027)` call with no kwargs and a `model_index.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.",
  "id": "GHSA-j7w6-vpvq-j3gm",
  "modified": "2026-05-14T20:53:57Z",
  "published": "2026-05-07T02:24:22Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/huggingface/diffusers/security/advisories/GHSA-j7w6-vpvq-j3gm"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-44827"
    },
    {
      "type": "WEB",
      "url": "https://github.com/huggingface/diffusers/pull/13448"
    },
    {
      "type": "WEB",
      "url": "https://github.com/huggingface/diffusers/commit/a37f6f8394ac2a7ee8360c3abea811efe54512b1"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/huggingface/diffusers"
    },
    {
      "type": "WEB",
      "url": "https://github.com/huggingface/diffusers/releases/tag/v0.38.0"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Diffusers has a `trust_remote_code` bypass via `custom_pipeline` and local custom components",
  "withdrawn": "2026-05-07T05:25:32Z"
}


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…
Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.


Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…