
В декодере Google Brotli нашли запись за границу буфера CVE-2026-111497 (node2shell, CVSS 9,2). Уязвимость Brotli в Python касается не только тех, кто импортирует пакет: urllib3, requests и aiohttp сами используют Brotli, если он установлен, Scrapy ставит его автоматически, а pip-audit, Dependabot и Safety пока молчат.

Коротко
- CVE-2026-111497 (node2shell, CVSS v4.0 9,2) — запись за границу буфера в декодере Google Brotli версий 1.0.3–1.2.0.
- В Python уязвимы Brotli ≤1.2.0 и brotlicffi 1.2.0.x; urllib3/requests, httpx[brotli] и aiohttp используют их, если пакеты установлены, а Scrapy ставит Brotli сам.
- На 23 октября pip-audit, Dependabot и Safety уязвимые версии не помечают — проверять придётся вручную.
- Исправленные версии: Brotli 1.2.1, brotlicffi 1.2.1.0, libbrotli 1.2.1, Node.js 22.23.4/24.22.1/26.12.1, Bun 1.4.4; образы и бинарники нужно пересобрать.
- Атаковать можно и сервер, и клиент: через входящий Content-Encoding: br и через ответы серверов, к которым обращается само приложение.
node2shell в Python: почему уязвимость касается не только Node.js
CVE-2026-111497 затрагивает всех, кто встраивает декодер Google Brotli версий с 1.0.3 по 1.2.0, на любой платформе. Первой об уязвимости сообщила команда Node.js — в среду, 21 октября, отсюда и прозвище node2shell.
В четверг на GitHub вышло официальное уведомление для Python: PyPI-пакеты Brotli (до 1.2.0 включительно) и brotlicffi (1.2.0.x) компилируют тот же декодер. К пятнице, 23 октября, стало ясно, что задеты и другие языки, и HTTP-сервисы, а исправления выходят не везде одновременно.
По CVSS v4.0 уязвимость получила 9,2 балла (Critical), проект Node.js оценивает её серьёзность как высокую. В записи CVE указаны две слабости: CWE-252 (непроверенное возвращаемое значение) и CWE-787 (запись за границу буфера). Последствия — повреждение памяти, утечка данных, падение процесса и, потенциально, выполнение произвольного кода. Аутентификация не нужна: достаточно, чтобы приложение распаковало подготовленный поток.
Самая неприятная деталь — хронология. OpenAI нашла ошибку ещё в феврале 2026 года, и тогда же её исправили в исходниках Brotli. Но правку не сочли исправлением безопасности, и релиза с ней не было до октября. Теперь каждый проект, который встраивает Brotli, выпускает обновление сам.
Как ошибка в декодере превращается в запись за границу буфера
Ошибка находится в функции DecodeBlockTypeAndLength(). Она обрабатывает переключение блоков — момент, когда поток Brotli меняет дерево Хаффмана. Функция возвращает обычный boolean, и BROTLI_FALSE означает сразу две разные вещи: «поток повреждён» и «нужно больше данных». В быстром режиме декодирования результат вообще игнорируется.
Атакующий собирает поток, который объявляет единственный тип блока и исчерпывает его длину. Так он вынуждает переключение, которое не может состояться. Дальше два исхода: декодер просит ещё данных для уже законченного потока либо продолжает разбор со старым состоянием блока. Второй вариант и приводит к записи за пределы рабочего буфера.
Атаковать можно и сервер, и клиент. Вредоносный клиент способен захватить необновлённый сервер, который распаковывает тела запросов. Вредоносный сервер — необновлённого клиента, который принимает сжатые ответы. В HTTP формат Brotli (RFC 7932) обозначается заголовком Content-Encoding: br, поэтому одна ошибка в нативном декодере задевает клиенты, серверы и прокси на разных языках.
ASLR и DEP/NX усложняют превращение повреждения памяти в выполнение кода, но саму запись за границу не отменяют. Падение нативного расширения обычно убивает весь процесс, а не один запрос, и перехват исключения в Python или JavaScript не спасает. В рабочей среде это отказ рабочих процессов, циклические перезапуски и деградация сервиса — даже без RCE.
Где уязвимость Brotli в Python прячется, если вы его не импортируете
Python-приложение уязвимо, если Brotli или brotlicffi просто установлены в окружении и данные атакующего доходят до декодера. Прямой import для этого не нужен:
- urllib3, а значит и requests, использует Brotli, если установлен любой из двух пакетов;
- httpx — при установке с extra brotli;
- aiohttp — если установлен Brotli;
- Scrapy устанавливает Brotli как зависимость автоматически.
Приложения на этих клиентах используют тот же уязвимый декодер. У Brotli на PyPI 82,8 млн загрузок в месяц — это мера распространённости пакета, а не число уязвимых серверов.
Данные атакующего попадают в декодер двумя маршрутами. Первый — исходящие запросы: приложение само обращается к серверу злоумышленника и получает от него Brotli-ответ. Так работают доставка вебхуков, предпросмотр ссылок, краулеры, функции вида «загрузить по URL».
Второй маршрут — входящие запросы, когда фреймворк или middleware распаковывает тело с Content-Encoding: br. В Node.js аналогичный путь проходит через Express 5, встроенный fetch() и body-parser.
Из практики. На пентестах входящий маршрут обычно находят сразу, а исходящий пропускают. Сервис, который «просто отправляет вебхук» или показывает предпросмотр ссылки, сам обращается к серверу, указанному внешним пользователем. В этом же классе функций мы регулярно находим SSRF, и проверять их стоит первыми.
Почему pip-audit, Dependabot и Safety молчат?
Сканеры молчат, потому что на 23 октября базы уведомлений об уязвимостях не связали CVE-2026-111497 с пакетами Brotli и brotlicffi в PyPI. pip-audit, Dependabot и Safety сверяют версии именно с этими базами. Записи нет — и проверка уязвимой версии завершается без предупреждений.
Путаницу усиливают примечания к выпускам: в описаниях исправленных Brotli 1.2.1 и brotlicffi 1.2.1.0 CVE не упоминается вовсе. Команда, которая читает changelog перед обновлением, не увидит повода торопиться. Пока базы не обновились, надёжный способ один — проверять версии вручную.
SBOM и lock-файлы тоже не дают полной картины. Они показывают объявленные зависимости, но пропускают статически слинкованные бинарники, промежуточные стадии многоэтапных сборок, самодостаточные исполняемые файлы и старые образы в реестре. Поэтому при аудите исходного кода мы сверяем зависимости с тем, что реально запущено, а не только с манифестом.
Как проверить версию Brotli в окружении, образе и бинарниках
Ищите Brotli версий 1.0.3–1.2.0 и brotlicffi 1.2.0.x везде, где реально работает код, а не только в репозитории.
- Окружение: «python -m pip show Brotli brotlicffi» в каждом venv и на каждом хосте, включая ноутбуки разработчиков и CI-раннеры.
- Lock-файлы: «grep -riE brotli requirements*.txt poetry.lock uv.lock Pipfile.lock». Важны и прямые, и транзитивные записи.
- Образы: «docker run –rm –entrypoint pip ОБРАЗ list | grep -i brotli» для каждого образа в кластере, включая ревизии, на которые настроен откат.
- Бинарники (частный пример): «curl -V» покажет версию Brotli, с которой собран curl. Универсальной команды нет: статические сборки curl/libcurl, исполняемые файлы Bun и приложения со своей копией библиотеки придётся проверять и пересобирать по отдельности.
Отдельный нюанс — wheel. Нативное расширение часто приходит уже скомпилированным, и обновлённая системная libbrotli не гарантирует, что расширение использует её, а не собственную копию. Проверяйте установленный пакет, а не только ОС.
Возможна и обратная ситуация: в дистрибутивах Linux исправление могут перенести в старую ветку без смены версии исходного проекта. Поэтому по номеру системного пакета выводы делать рано — сверяйтесь с changelog дистрибутива.
Brotli 1.2.1 закрывает node2shell? Какие версии исправлены
Да, Brotli 1.2.1 и brotlicffi 1.2.1.0 закрывают уязвимость: «pip install -U ‘Brotli>=1.2.1’ ‘brotlicffi>=1.2.1.0’». Если используете Poetry, uv или pip-tools, обновите ограничение и пересоберите lock-файл.
В исходном проекте исправление внесено коммитом 0ed4268 в google/brotli. Функция теперь возвращает код ошибки. Добавлен код BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH (-17): при ошибке формата декодирование останавливается и в безопасном, и в быстром режиме.
По данным Aikido, специалисты OSM сравнили исходные дистрибутивы Brotli 1.2.0 и 1.2.1: помимо смены номера версии, они отличаются только изменениями декодера из этого коммита. Иначе говоря, обновление до 1.2.1 не тянет за собой посторонних изменений.
| Продукт | Исправленные версии |
|---|---|
| libbrotli | 1.2.1; для старых линий 1.0.10 и 1.1.1 |
| Python | Brotli 1.2.1, brotlicffi 1.2.1.0 |
| Node.js | 22.23.4, 24.22.1, 26.12.1 |
| Bun | 1.4.4 |
| Envoy | 1.40.0, 1.39.3, 1.38.6, 1.37.8 (1.36.12 тоже исправлена, но ветка не поддерживается) |
| Istio | 1.29.9, 1.30.6, 1.31.2 (поддержка ветки 1.29 закончилась) |
| curl/libcurl | обновить Brotli до 1.2.1 либо установить исправленный пакет дистрибутива; статические сборки — пересобрать |
Стандартный mod_brotli в Apache Brotli не декодирует, Nginx по умолчанию тоже. Сторонние модули и собственные сборки стоит проверить отдельно.
Проверять нужно сборку, которая фактически развёрнута. Контейнеры и бинарники с уязвимой копией остаются уязвимыми до пересборки. После обновления удалите старые ревизии, чтобы автомасштабирование или откат не вернули их в работу. Чтобы такая проверка шла при каждой сборке, её встраивают в конвейер при внедрении DevSecOps.
Что делать, если обновить Brotli нельзя?
Если обновление задерживается, уберите декодер с пути данных. Варианты — по убыванию надёжности:
- удалить Brotli и brotlicffi из окружения и убрать extras вроде httpx[brotli], затем пересобрать lock-файлы. Поддержка gzip и deflate сохранится, но автоматический переход на эти форматы не гарантирован: сервер, который всё равно пришлёт Brotli, вызовет нечитаемый ответ или ошибку приложения;
- отклонять входящие запросы, где Content-Encoding содержит br, до того как сработает любой декодер. Сравнивать без учёта регистра и с разбором списков вроде «gzip, br»;
- для исходящих запросов убрать br из Accept-Encoding и отбрасывать Brotli-ответы до декодирования. Одного Accept-Encoding мало: вредоносный сервер всё равно пришлёт Brotli, а urllib3 выбирает декодер по Content-Encoding ответа;
- ограничить исходящий трафик сервисов с вебхуками, предпросмотром ссылок и краулерами.
Удаление Python-пакетов не трогает копии Brotli, встроенные в другие среды выполнения, библиотеки и продукты, — их закрывают отдельно.
Если есть подозрение, что сервис уже атаковали, ищите не сообщения от Brotli, а SIGSEGV и SIGABRT, core dumps, перезапуски рабочих процессов и контейнеров в истории оркестратора. Падение нативного кода часто не оставляет в логах приложения полезного traceback. Публичные PoC в рабочей среде не запускайте — только в изолированном экземпляре без секретов и сети. Если следы нашлись, подключайте реагирование на инциденты.
Что пока неизвестно
Неизвестно, когда базы уведомлений привяжут CVE к PyPI-пакетам и сканеры начнут его видеть. Список исправлений для других языков ещё не окончательный: в пятничном обновлении Aikido прямо говорится, что новые патчи на подходе. Неясно и то, как быстро обновятся проекты, которые встроили Brotli прямо в свой код, и управляемые сервисы — там исправление нужно подтверждать у провайдера.
Частые вопросы
Правда ли, что уязвимость в Brotli затрагивает Python?
Да. Пакеты Brotli (до 1.2.0) и brotlicffi (1.2.0.x) с PyPI компилируют тот же уязвимый декодер, что и Node.js. Официальное уведомление об этом вышло на GitHub на следующий день после раскрытия в Node.js.
Почему Dependabot молчит, хотя версия Brotli уязвима?
На 23 октября базы уведомлений об уязвимостях не связали CVE-2026-111497 с пакетами Brotli и brotlicffi в PyPI. Dependabot, pip-audit и Safety опираются на эти базы, поэтому версии нужно проверять вручную.
Уязвим ли requests, если Brotli не импортируется напрямую?
Да, если в окружении установлен Brotli или brotlicffi: urllib3, на котором построен requests, автоматически использует их для ответов с Content-Encoding: br. Обновлять нужно сам Brotli до 1.2.1, а не requests.
Достаточно ли обновить только пакет Brotli?
Для Python-окружения — да, если обновлены и Brotli (до 1.2.1), и brotlicffi (до 1.2.1.0). Но контейнерные образы, статические сборки curl и исполняемые файлы Bun со своей копией библиотеки остаются уязвимыми до пересборки.
Можно ли временно отключить Brotli?
Можно удалить Brotli и brotlicffi, отклонять запросы с Content-Encoding: br и отбрасывать Brotli-ответы до декодирования. Убрать br только из Accept-Encoding недостаточно: вредоносный сервер всё равно пришлёт сжатый ответ.
Почему нельзя ждать, пока сканеры начнут видеть CVE?
Исправление лежало в исходниках с февраля, а уязвимость позволяет удалённо и без аутентификации повредить память процесса. Пока базы не обновлены, уязвимые сборки проходят проверки без предупреждений, поэтому обновляться стоит сразу, начиная с сервисов, которые принимают сжатые тела или обращаются к внешним серверам.
Если нужно быстро понять, где в ваших сервисах работают нативные зависимости вроде Brotli и до каких из них может дотянуться атакующий, SecurityLab.pro проведёт проверку зависимостей и аудит ИБ с разбором реальных путей данных.
Источники
- Aikido Security — разбор CVE-2026-111497 в Python-пакетах
- google/brotli — коммит 0ed4268
- Node.js — бюллетени безопасности
- NVD — CVE-2026-111497
- RFC 7932 — Brotli Compressed Data Format