Флаг с названием trust_remote_code обещает простую вещь: чужой код не запустится, пока разработчик сам не скажет "доверяю". Именно на это обещание опираются те, кто скачивает модели из чужих репозиториев и считает такие репозитории пассивными хранилищами весов. 27 октября 2026 года вышла версия Diffusers 0.42.1, и её единственная цель в том, чтобы закрыть случай, когда обещание не выполнялось: имя библиотеки компонента из конфигурационного файла попадало в импорт без проверки. Ниже разобрано, что именно исправлено и при каких условиях это работало, чем эта история похожа на майскую волну уязвимостей того же класса, какие неудобства несёт само обновление и как за вечер проверить свои установки скриптом и закрыть основные пути минимальными мерами. Скрипт аудита приложен и проверен на тестовых каталогах.
Версия 0.42.1 от 27 октября исправляет разбор имени библиотеки компонента, который обходил флаг trust_remote_code
Релиз 0.42.1 помечен как исправление безопасности и привязан к рекомендации GHSA-4prv-5hj7-c2xr. Затронуты версии 0.42.0 и все более ранние, исправленная версия одна, 0.42.1. Оценка опасности по шкале CVSS составляет 7,0, то есть высокий уровень, но заметно ниже, чем у майских уязвимостей, о которых речь пойдёт дальше. Вектор оценки говорит о локальном векторе, высокой сложности, отсутствии требуемых привилегий и необходимости действия пользователя. Идентификатор CVE на момент публикации не назначен. Классификация слабости - небезопасная рефлексия и включение функциональности из недоверенной области. Тестовый пример проблемы датирован 17 октября, публикация состоялась через десять дней.
Суть правки скромно описана в самой заметке к релизу: изменён способ, которым определяется библиотека, объявленная у компонента при загрузке через from_pretrained. Конфигурации пайплайнов и моделей описывают каждый компонент парой из имени библиотеки и имени класса. Правка касается трёх путей загрузки: DiffusionPipeline, AutoModel и ModularPipeline. Заодно добавлено понятное сообщение об ошибке, если объявленный класс не найден в найденном модуле. Установка исправления выглядит так:
pip install "diffusers>=0.42.1"
Версии ветки 0.42 требуют Python 3.11 или новее, и это важная деталь, к которой мы вернёмся: для части окружений обновление пакета означает обновление интерпретатора. Для остальных достаточно одной команды, а проверить результат можно строкой python -c "import diffusers; print(diffusers.version)".
Две лазейки в конфигурациях превращали имя модуля в строку для импорта, и сработать они могли только у локальной копии репозитория
Первая лазейка: имя библиотеки компонента из файла model_index.json или modular_model_index.json в ветке загрузки из локального каталога импортировалось напрямую. Список разрешённых библиотек, который в этом файле используется в других местах, на эту ветку не распространялся. Вторая лазейка: ключи словаря requirements из файла modular_config.json обрабатывались как имена модулей через поиск спецификации. У составных имён с точками импортируются родительские пакеты, а вместе с ними выполняются их файлы init.py.
Ключевое условие описано в рекомендации чётко. Каталог репозитория должен быть доступен для импорта, то есть лежать в одном из мест, где Python ищет модули. Пример из рекомендации: человек клонирует репозиторий модели или скачивает его локально и запускает свой скрипт из родительского каталога. Тогда файл Python, лежащий внутри репозитория, может выполниться при вызове from_pretrained с параметрами по умолчанию. Холодная загрузка по идентификатору репозитория с хаба, без локального каталога, в рекомендации названа не затронутой. Запуск с переменной PYTHONSAFEPATH=1 атаку блокирует.
Если переложить это на бытовой язык, то Python ищет модули в списке папок, как человек ищет ключи по карманам. Если папка с чужой моделью оказалась в этом списке, то слово из чужого файла настроек может указать прямо на чужой файл с кодом. Проверка доверия стоит у главной двери, а такой путь идёт через боковую.
Последствия описаны жёстко. Код выполняется с правами пользователя в момент вызова from_pretrained, ещё до чтения весов, и ему доступно всё, что доступно процессу: токен доступа к хабу, исходники проекта, кэш моделей, сеть. Обход срабатывает даже при trust_remote_code=False и при переменной DIFFUSERS_DISABLE_REMOTE_CODE=1, а закрепление ревизии и формат safetensors на него не влияют. Формальный обходной путь в рекомендации не назван, кроме упомянутой переменной PYTHONSAFEPATH.
Понятно, почему оценка 7,0, а не 8,8. Локальный вектор, высокая сложность и необходимость действия пользователя отсекают массовую эксплуатацию: чтобы сработать, нужен определённый порядок действий жертвы. Но именно такой порядок, клонировать репозиторий рядом с кодом и запустить скрипт, и встречается в домашних лабораториях, учебных проектах и ноутбуках исследователей постоянно.
Это уже вторая волна проблем одного класса, и в мае три обхода закрыли в версии 0.38.0 по той же схеме ошибок
Весной 2026 года исследователи описали группу из нескольких обходов под общим названием FaceHugger. Среди них CVE-2026-44513 с оценкой 8,8 и CVE-2026-45804 с оценкой 7,5, плюс родственная ошибка, при которой отсутствие пользовательского пайплайна превращалось в имя файла None.py. Исправление вышло в версии 0.38.0 первого мая через запрос на слияние номер 13448, публичный разбор появился 27 июля. Версии до 0.38.0 затронуты, и в закрытых рекомендациях подчёркивается, что при trust_remote_code=False защиты на них не было.
Общая причина там формулировалась так: проверка доверия стояла в функции download(), а динамическая загрузка модулей, исполняющая код, жила ниже по цепочке и проверку не повторяла. Один из вариантов - гонка проверки и использования: между двумя сетевыми запросами при загрузке оставалось короткое окно, за которое конфигурацию репозитория можно было подменить. Исправление перенесло проверку в функцию get_cached_module_file, через которую проходит любая загрузка динамического модуля.
Сопоставление двух историй даёт простой вывод. Исправление 0.42.1 говорит о той же болезни: проверка стоит у одной двери, а код способен войти через другие. Каждое такое исправление закрывает конкретный ход, но не меняет архитектуру. Поэтому рассчитывать на флаг как на единственный замок нельзя. Флаг - табличка, которая хорошо работает против случайной ошибки и плохо против целенаправленно подготовленного репозитория.
За обновление до 0.42.1 приходится платить переходом на Python 3.11, новой версией клиента хаба и правками в модульных пайплайнах
Версия 0.42.0, на которой основан патч, принесла много нового. В ней появились FLUX.3 Image Klein для генерации и редактирования картинок с референсами, Bria Fibo 2.0 с описаниями в формате структурированного JSON, Kandinsky 6 для видео с синхронным звуком и отдельный конвейер повышения разрешения видео, а также SANA-WM для видео с управляемой камерой. Для пользователей всё это приятная часть. Вторая часть - совместимость.
Python 3.10 больше не поддерживается: теперь требуется 3.11 или новее. Диапазон версии клиента хаба изменён на >=1.32.0,<3.0. В модульных пайплайнах для FLUX, Krea 2 и LTX-Video латенты теперь распаковываются внутри основных блоков шумоподавления, и самописные рабочие процессы должны передавать в блоки декодирования уже распакованные латенты, а запасной вариант с упакованными латентами объявлен устаревшим и будет убран в 0.47.0. Для квантизации через TorchAO нужна версия 0.15 или новее.
Практический вывод для администратора такой. Если окружение на Python 3.10, одной командой pip не обойтись: нужен новый интерпретатор, пересборка зависимостей и проверка всех мест, где используются модульные пайплайны. Поэтому порядок действий лучше такой: собрать отдельное окружение, поставить diffusers в диапазоне от 0.42.1 до 0.43, прогнать рабочие сценарии, и только потом менять основное. В это время защита вместо обновления тоже возможна, и о ней говорится в разделе про минимальные меры.
Скрипт аудита находит устаревшую версию, чужой код в репозиториях моделей и подозрительные имена в конфигурациях
Скрипт не пытается доказывать наличие вредоносного кода, он ищет признаки, при которых стоит остановиться и посмотреть. Проверок пять. Первая сверяет установленную версию diffusers с порогом 0.42.1. Вторая читает model_index.json и modular_model_index.json в каждом найденном репозитории и проверяет имена библиотек компонентов: имя, не похожее на имя пакета, помечается как HIGH, а неизвестная библиотека как REVIEW. Третья разбирает словарь requirements в modular_config.json по той же логике. Четвёртая ищет файлы Python внутри каталогов моделей: обычному репозиторию весов код не нужен, поэтому любой такой файл заслуживает взгляда, а файл None.py сразу получает уровень HIGH. Пятая проверяет, лежит ли репозиторий модели рядом с текущим каталогом при незаданной PYTHONSAFEPATH.
"""Аудит локальных сетапов Diffusers: версия пакета, чужой код в репозиториях, пути импорта."""
import json
import os
import re
import sys
from importlib import metadata
from pathlib import Path
FIXED = (0, 42, 1)
ALLOWED_LIBS = {"diffusers", "transformers", "peft", "onnxruntime", "torch"}
INDEX_NAMES = ("model_index.json", "modular_model_index.json")
SAFE_CHARS = re.compile(r"^[A-Za-z0-9_.\-]+$")
def parse_version(text):
nums = [int(n) for n in re.findall(r"\d+", text)[:3]]
return tuple(nums) + (0,) * (3 - len(nums))
def check_version():
try:
version = metadata.version("diffusers")
except metadata.PackageNotFoundError:
return []
if parse_version(version) < FIXED:
return [("HIGH", f"diffusers {version}: нужно обновить до 0.42.1 или новее")]
return []
def check_library_name(where, key, lib):
if lib is None:
return []
if not isinstance(lib, str) or ".." in lib or not SAFE_CHARS.match(lib):
return [("HIGH", f"{where}: компонент {key}, странное имя библиотеки {lib!r}")]
if lib not in ALLOWED_LIBS:
return [("REVIEW", f"{where}: компонент {key}, неизвестная библиотека {lib!r}")]
return []
def check_index(path):
try:
doc = json.loads(path.read_text(encoding="utf-8"))
except (OSError, ValueError):
return [("REVIEW", f"{path}: файл не читается как JSON")]
found = []
for key, value in doc.items():
if key.startswith("_") or not isinstance(value, list) or not value:
continue
found += check_library_name(path, key, value[0])
return found
def check_requirements(path):
try:
doc = json.loads(path.read_text(encoding="utf-8"))
except (OSError, ValueError):
return [("REVIEW", f"{path}: файл не читается как JSON")]
found = []
for name in (doc.get("requirements") or {}):
if not SAFE_CHARS.match(name) or ".." in name:
found.append(("HIGH", f"{path}: странное имя в requirements {name!r}"))
elif "." in name:
found.append(("REVIEW", f"{path}: имя с точкой в requirements {name!r}"))
return found
def check_python_files(folder):
found = []
for root, _dirs, files in os.walk(folder):
for name in files:
if not name.endswith(".py"):
continue
level = "HIGH" if name == "None.py" else "REVIEW"
found.append((level, f"{Path(root) / name}: код внутри репозитория модели"))
return found
def scan(roots):
found = []
for top in roots:
for root, _dirs, files in os.walk(top):
names = set(files)
if not names & {*INDEX_NAMES, "modular_config.json"}:
continue
folder = Path(root)
for name in INDEX_NAMES:
if name in names:
found += check_index(folder / name)
if "modular_config.json" in names:
found += check_requirements(folder / "modular_config.json")
found += check_python_files(folder)
return found
def check_import_path(cwd):
if os.environ.get("PYTHONSAFEPATH"):
return []
found = []
for folder in [cwd, *sorted(p for p in cwd.iterdir() if p.is_dir())]:
if any((folder / name).exists() for name in INDEX_NAMES):
found.append(("REVIEW", f"{folder}: репозиторий модели рядом с кодом, "
"а PYTHONSAFEPATH не задан"))
return found
def main(roots):
findings = check_version() + scan(roots) + check_import_path(Path.cwd())
for level, text in sorted(set(findings)):
print(f"[{level}] {text}")
if not findings:
print("Замечаний нет")
return 1 if any(level == "HIGH" for level, _ in findings) else 0
if __name__ == "__main__":
default = [Path.home() / ".cache" / "huggingface" / "hub"]
sys.exit(main([Path(p) for p in sys.argv[1:]] or default))
Запуск без аргументов проверяет стандартный кэш моделей, запуск с путями проверяет перечисленные каталоги, например python audit_diffusers.py ~/models ~/comfy/models. Код возврата равен 1, если найдено хотя бы одно замечание уровня HIGH, так что скрипт можно поставить в проверку перед сборкой образа или в ночное задание.
Проверка шла на тестовом наборе из четырёх каталогов: чистый репозиторий, репозиторий с подозрительными именами и лишними файлами, конфигурация с необычными именами в requirements и каталог с битым JSON. Результат оказался таким:
[HIGH] models/bad/None.py: код внутри репозитория модели
[HIGH] models/bad/model_index.json: компонент num, странное имя библиотеки 5
[HIGH] models/bad/model_index.json: компонент x, странное имя библиотеки '../up'
[HIGH] models/mod/modular_config.json: странное имя в requirements 'a/b'
[REVIEW] models/bad/__init__.py: код внутри репозитория модели
[REVIEW] models/bad/model_index.json: компонент vae, неизвестная библиотека 'evil.pkg'
[REVIEW] models/empty/model_index.json: файл не читается как JSON
[REVIEW] models/mod/modular_config.json: имя с точкой в requirements 'ruamel.yaml'
Чистый репозиторий не дал ни одного замечания. Версия сверялась на значениях 0.41.0, 0.42.0, 0.42.1 и 0.43.0: первые две помечены, остальные нет, а при отсутствии пакета проверка тихо пропускается. С переменной PYTHONSAFEPATH=1 пятая проверка молчит. Нужны и оговорки. Список ALLOWED_LIBS стоит дополнить библиотеками, которые действительно встречаются в доверенных репозиториях, иначе уровень REVIEW будет шумным. Файлы Python в репозитории бывают и законными, например в репозиториях с пользовательскими пайплайнами, поэтому их наличие не доказательство, а повод для чтения. Скрипт тестировался только на искусственных каталогах и не заменяет разбор настоящих рекомендаций.
Минимальный набор мер из пяти шагов закрывает главные пути, даже если обновление придётся отложить
Защита строится так, чтобы каждая мера работала независимо от остальных и от самой библиотеки. Порядок такой:
- Обновить diffusers до 0.42.1 в окружениях, контейнерных образах и файлах блокировки зависимостей;
- Запускать скрипты с переменной PYTHONSAFEPATH=1 или ключом -P, а на Python 3.10 с изолированным режимом -I, и держать каталоги моделей вне путей импорта;
- Скачивать модели отдельным шагом по идентификатору и фиксированной ревизии, а загружать из локальной копии только после прогона скрипта аудита;
- Держать список проверенных репозиториев и обёртку, которая отказывает всем остальным;
- Первую загрузку незнакомой модели делать в одноразовом контейнере без сети и без токенов.
Второй шаг стоит пояснить. Переменная PYTHONSAFEPATH появилась в Python 3.11 и отключает добавление каталога скрипта и текущего каталога в пути импорта, ключ -P делает то же самое. На версии 3.10 такого нет, но есть изолированный режим -I: он тоже не добавляет каталог скрипта, зато заодно игнорирует переменные окружения и пользовательские пакеты, так что подходит для виртуальных окружений, где все зависимости лежат внутри. Эти меры работают как временный заслон для тех, кто пока не может поднять версию Python.
Третий и четвёртый шаги можно оформить небольшой обёрткой. Она не защищает от ошибок библиотеки, это только политика: в проект попадают лишь модели, которые кто-то уже просмотрел.
from diffusers import DiffusionPipeline
REVIEWED = {
"org/model-a": "0123456789abcdef0123456789abcdef01234567",
}
def load_reviewed(repo_id, **kwargs):
revision = REVIEWED.get(repo_id)
if revision is None:
raise PermissionError(f"репозиторий {repo_id} не проходил проверку")
return DiffusionPipeline.from_pretrained(
repo_id, revision=revision, trust_remote_code=False, **kwargs)
Закрепление ревизии даёт повторяемость и гарантирует, что загружается то, что просмотрели. Но, как отмечено в рекомендации, ошибка 0.42.0 от закрепления ревизии не зависит, поэтому обёртка работает в паре с обновлением, а не вместо него.
Пятый шаг строится на разделении двух операций, скачивания и загрузки. Скачивание файлов само по себе код не исполняет, и его можно делать в контейнере с сетью, но без токенов и без монтирования исходников. Загрузка идёт в другом контейнере, без сети, с каталогом моделей только для чтения:
hf download org/model-a --revision 0123456789abcdef0123456789abcdef01234567 --local-dir models/model-a
docker run --rm --network none --read-only --cap-drop ALL \
--security-opt no-new-privileges --tmpfs /tmp \
-v "$PWD/models:/models:ro" -v "$PWD/test_load.py:/app/test_load.py:ro" \
my-diffusers:0.42.1 python -P /app/test_load.py
Здесь образ my-diffusers:0.42.1 нужно собрать самому, в нём стоит исправленная версия библиотеки. Контейнер без сети не отправит ничего наружу, даже если загрузка зацепит чужой код, каталог только для чтения не даст изменить файлы, а отсутствие токена лишает атакующего главной добычи. Параметр -P отдельно закрывает путь импорта, о котором шла речь выше.
Что проверить в сетапах с ComfyUI и самописными нодами, где репозитории моделей лежат рядом с кодом
Установка пользовательской ноды сама по себе означает запуск чужого кода с правами пользователя, и этот риск понимают все, кто ею занимается. Особенность описанной ошибки в том, что она превращала в источник кода репозиторий, который обычно считают просто хранилищем файлов: каталог с весами и конфигурацией. Человек, который тщательно выбирает ноды, мог не думать о папке с моделью, скачанной ради эксперимента, и клонировал её прямо в рабочий каталог проекта. Поэтому в домашних сетапах стоит пройтись по нескольким местам.
Первое место - расположение моделей. Каталоги с model_index.json не должны лежать в корне проекта, рядом со скриптами и в каталогах нод. Их место в отдельной папке, не входящей в пути импорта, и именно её передаёт скрипт аудита. Второе место - вызовы загрузки. Найти их в пользовательских нодах можно простой командой:
grep -rn "trust_remote_code\|from_pretrained" custom_nodes/ --include="*.py"
Результат стоит просмотреть на предмет локальных путей, значений trust_remote_code=True и мест, где идентификатор репозитория приходит от пользователя интерфейса: такой путь позволяет подставить любой репозиторий. Часть нод и обвязок использует diffusers под капотом, и узнать об этом можно только по коду. Третье место - окружение процесса: токены хаба и облачные ключи не должны быть доступны процессу, которому нужно только генерировать картинки. Если токен нужен для скачивания закрытых моделей, его держат только в контейнере скачивания.
Наконец, полезно просто спросить себя, что лежит рядом с интерпретатором: каталог кэша, исходники, ключи. Именно это достаётся тому, кто смог выполнить код при загрузке, и именно это по возможности убирают из доступа. Если Diffusers обновлён, пути импорта закрыты, модели проверены, а загрузка идёт в контейнере без сети, то одна пропущенная ошибка библиотеки перестаёт быть катастрофой.
Главный вывод этой истории не в том, что одна версия лучше другой. Граница доверия проходит не по флагу в вызове, а по каталогу, в котором лежит чужой репозиторий, и по правам процесса, который его открывает. Флаг trust_remote_code остаётся полезной табличкой, но замками служат контейнер, версия и порядок работы.