
Я нашёл уязвимость нулевого дня в langchain — вот как всё прошло
Как забытый устаревший API в одном из самых популярных фреймворков ИИ тихо раскрыл миллионы приложений для произвольного чтения файлов.
Есть такое чувство, когда proof-of-concept срабатывает с первой попытки. Не совсем волнение — скорее медленное, неприятное осознание того, что произошло что-то реальное. Именно это я почувствовал, когда запустил свою тестовую конфигурацию против load_prompt_from_config() и увидел, как содержимое файла, который она не имела права читать, выводится прямо в мой терминал.
Это история CVE-2026-34070: уязвимость обхода пути в langchain-core, исправленная в версии 1.2.22.
Если вы создавали что-либо с ИИ за последние пару лет, вы почти наверняка сталкивались с LangChain. Это соединительная ткань современного стека ИИ — фреймворк, который связывает языковые модели, векторные хранилища, инструменты и управление промптами в целостные приложения. С более чем 130k звёзд на GitHub и внедрением во всём — от небольших проектов выходного дня до корпоративных развёртываний — уязвимость здесь не остаётся изолированной.
Я проводил ревизию кода подсистемы промптов, когда мне бросилось в глаза: модуль под названием langchain_core/prompts/loading.py. Он загружал файлы с диска на основе значений, взятых напрямую из десериализованных словарей конфигурации. Без валидации. Без санитизации путей. Просто .
open(path)Я продолжил читать.
В центре этого находились три внутренние функции:
_load_template() — читает файлы, на которые ссылаются template_path, suffix_path и prefix_path_load_examples() — читает файлы, на которые ссылается ключ examples, когда он является строкой_load_few_shot_prompt() — читает файлы, на которые ссылается example_prompt_pathПроверки расширений файлов были на месте. .txt для шаблонов. .json, .yaml, .yml для примеров. Но ничто не мешало этим путям быть абсолютными (/etc/passwd) или основанными на обходе (../../../../home/user/.ssh/). Фильтр расширений давал ложное чувство безопасности — он просто означал, что атакующему нужно выбрать правильное расширение, а не то, что атака заблокирована.
Все эти функции доступны через два публичных API: load_prompt() и load_prompt_from_config().
Самая простая версия выглядела так:
from langchain_core.prompts.loading import load_prompt_from_config
config = {
"_type": "prompt",
"template_path": "/tmp/secret.txt",
"input_variables": [],
}
prompt = load_prompt_from_config(config)
print(prompt.template) # содержимое /tmp/secret.txt, выведено чисто
Вот и всё. Передайте абсолютный путь — получите содержимое файла, обёрнутое в PromptTemplate. Без аутентификации. Без особых привилегий. Если вы можете влиять на словарь конфигурации, вы можете читать файлы.
Обход каталогов работал так же чисто:
config = {
"_type": "prompt",
"template_path": "../../etc/secret.txt",
"input_variables": [],
}
Вариант с JSON/YAML был, пожалуй, более опасным из-за типов файлов, к которым он мог получить доступ:
config = {
"_type": "few_shot",
"examples": "../../../../.docker/config.json",
"example_prompt": {
"_type": "prompt",
"input_variables": ["input", "output"],
"template": "{input}: {output}",
},
"prefix": "",
"suffix": "{query}",
"input_variables": ["query"],
}
prompt = load_prompt_from_config(config)
.docker/config.json содержит ваши учётные данные Docker Hub. ~/.azure/accessTokens.json содержит ваши токены Azure. Манифесты Kubernetes, конфигурации CI/CD, внутренние настройки приложений — любой файл с правильным расширением, находящийся где угодно в файловой системе, был доступен.
Оценка CVSS составила 7.5 High с вектором AV:N/AC:L/PR:N/UI:N — доступный по сети, низкая сложность, без привилегий, без необходимости взаимодействия с пользователем.
Оценка ограничена ниже 9+, потому что проверка расширений файлов действительно ограничивает, какие файлы можно прочитать. Но 7.5 всё равно занижает реальное влияние в определённых моделях развёртывания.
Подумайте, где это приземляется в продакшене:
load_prompt_from_config(), каждый пользователь — потенциальный атакующий.В таких средах ограничение «расширением файла» имеет гораздо меньшее значение. Атакующий может просто нацелиться на файлы, о существовании которых он знает, с правильным расширением. На типичном облачном экземпляре: requirements.txt, config.yaml, .env.yaml, смонтированные секретные файлы с расширением .json — список длинный.
Затронутые функции описаны в бюллетене как «недокументированные устаревшие API». Они предшествуют текущей системе сериализации langchain_core.load (dumpd/dumps/load/loads), которая использует модель на основе белого списка и не выполняет чтение файловой системы.
Новые API существуют. Они лучше. Но старый код никогда не был очищен — он просто лежал там, доступный, без валидации, ожидая.
Это паттерн, на который стоит обратить внимание. В быстро развивающихся проектах с открытым исходным кодом, особенно в тех, которые росли так же быстро, как LangChain, технический долг накапливается в углах. Устаревший код, который «никогда не предназначался для пользователей», не получает такого же внимания, как основная поверхность API. Но он всё ещё вызываемый. Он всё ещё в пакете. И если он читает файлы с диска, это потенциальная уязвимость.
Патч вышел в langchain-core 1.2.22. Исправление добавляет валидацию путей, которая отклоняет как абсолютные пути, так и последовательности обхода .. до открытия любого файла. Запасной выход — allow_dangerous_paths=True — доступен для приложений, которым действительно нужно читать из доверенных путей, с явным признанием того, что вызывающий код сознательно идёт на риск.
Устаревшие API также официально объявлены устаревшими в этом выпуске. Они будут полностью удалены в 2.0.0. Если вы используете load_prompt() или load_prompt_from_config() где-либо, мигрируйте на эквиваленты langchain_core.load сейчас, а не ждите ломающего изменения.
Обновитесь немедленно:
pip install --upgrade langchain-core
Проверьте, что у вас версия 1.2.22 или новее:
python -c "import langchain_core; print(langchain_core.__version__)"
Если вы строите на LangChain, стоит провести быстрый аудит:
load_prompt и load_prompt_from_config в вашей кодовой базе. Если они встречаются, проверьте, что в них передаётся..txt, .json или .yaml существуют на ваших экземплярах и что они содержат.Уязвимости безопасности в ИИ-инфраструктуре будут иметь всё большее значение, а не меньшее, по мере того как эти системы обрабатывают более чувствительные рабочие нагрузки. Команда LangChain отреагировала хорошо — исправление чистое, путь устаревания ясен, а документация бюллетеня тщательная.
Но это хорошее напоминание о том, что поверхность атаки ИИ-приложения — это не только модель. Это каждая библиотека в стеке, каждая устаревшая функция, которую так и не очистили, каждое место, где «пользователь, вероятно, не передаст сюда недоверенный ввод» оказалось предположением, а не гарантией.
Читайте код. Особенно старые части.
CVE-2026-34070 была присвоена этой уязвимости. Полный бюллетень доступен в LangChain GitHub Security Advisory.