21 октября 2026 Node.js проекта выпустил security-релиз, закрывающий CVE-2026-111497 - out-of-bounds write в декодере Brotli, встроенном в рантайм; эта уязвимость получила имя Node2Shell.[1][2] Вам это важно, если вы self-hostите: уязвимый код - это не npm-пакет, который можно найти grep’ом. Он живёт в C-библиотеке, которую по умолчанию таскают с собой Node, Bun, Python и даже proxy-рантаймы, и он достижим по HTTP с обеих сторон соединения: вредоносный клиент может взять ваш сервер, а вредоносный сервер - взять ваш клиент.[3]

Вот короткая версия, потом детали:

  • Уязвимы: Google Brotli 1.0.3–1.2.0 - а значит Node.js на ветках 22.x / 24.x / 26.x до октябрьского фикса, Bun ≤ 1.4.3, Python brotli / brotlicffi ≤ 1.2.0, а также сборки Envoy / Istio, которые декодируют Brotli.[2][4][5]
  • Исправлено: Brotli 1.0.10, 1.1.1, 1.2.1; Node.js 22.23.4 / 24.22.1 / 26.12.1; Bun 1.4.4; Python brotli 1.2.1 / brotlicffi 1.2.1.0; Envoy ≥ 1.40.0 / 1.39.3 / 1.38.6 / 1.37.8 / (1.36.12); Istio 1.31.2 / 1.30.6.[1][4][5][6][7][8][9]
  • Severity: 9.2 (Critical) по CVSS 4.0 по данным NVD; первопричина - проигнорированный возврат значения (CWE-252), который превращается в out-of-bounds write (CWE-787).[2][10]

Что это за баг

Дефект живёт в c/dec/decode.c C-библиотеки Google Brotli. Декодер Brotli - конечный автомат: поток делится на метаблоки, внутри каждого метаблока декодер ведёт блоки типов для литералов, команд и дистанций, у каждого - своя Хаффман-дерево. Небольшой кольцевой буфер (block_type_rb) хранит последние индексы типов блоков, чтобы декодер выбирал нужное дерево.[3][11]

Конкретный сбой - в DecodeBlockTypeAndLength(). Когда запрошен переключение типа блока, но заголовок метаблока объявил, что дерева типов не существует (max_block_type <= 1), функция корректно возвращает BROTLI_FALSE. Проблема - в вызывающем коде: в быстром (не-safe) пути макрос BROTLI_SAFE отбрасывает это возвращаемое значение, и декодер продолжает работу с устаревшим состоянием. Дальше он индексирует htree_command = s->block_type_rb[3] - индекс теперь устаревший и за границей, когда реально есть только один слот Хаффман-дерева.[11]

Именно этот устаревший, вышедший за границы доступ и есть запись, которая может повредить соседнюю память состояния декодера. Вот описание Google простыми словами: удалённый неаутентифицированный атакующий может с помощью специально сформированного Brotli-потока повредить соседнюю память состояния, раскрыть чувствительные данные, вызвать отказ в обслуживании или потенциально выполнить произвольный код.[2]

Google как CVE Numbering Authority оценивает её в 9.2 по CVSS 4.0 - Critical. Node.js в своём адвизори оценивает влияние на собственный рантайм как High.[1][2][10] Апстрим-фикс был закоммичен в исходники Brotli 27 февраля, но ни один релиз не принёс его до 21 октября - семимесячное окно, в котором каждый потребитель уязвимой библиотеки поставлял дыру открытой.[3]

Почему это хуже, чем «баг Node»

Жертва - это декодер Brotli, и встраивают его куда больше, чем Node. Вот карта затронутости, сверенная с первичными адвизори 25 октября 2026:

Компонент Уязвим Исправлено Путь экспонирования
Google Brotli (C-библиотека) 1.0.3 – 1.2.0 1.0.10, 1.1.1, 1.2.1 любая программа, вызывающая декодер[2][9]
Node.js 22.x / 24.x / 26.x до фикса 22.23.4, 24.22.1, 26.12.1 Brotli-API node:zlib; fetch() ответа с Content-Encoding: br; Express/body-parser, декодирующие br-тело запроса[1][3]
Bun ≤ 1.4.3 1.4.4 (встраивает Brotli 1.2.1) fetch() br-ответа, совместимые с node:zlib API[6]
Python brotli ≥ 1.0.3, < 1.2.1 1.2.1 декодирование запросов/ответов HTTP-клиентами, когда пакет установлен[4]
Python brotlicffi < 1.2.1.0 1.2.1.0 то же[4]
Envoy 1.39.2, 1.38.5, 1.37.7, 1.36.11 1.40.0, 1.39.3, 1.38.6, 1.37.8, (1.36.12 не поддерживается) только сборки с включённым декомпрессором Brotli[5]
Istio те, что несут уязвимый Envoy 1.31.2, 1.30.6 когда EnvoyFilter включает декомпрессию Brotli[7][8]

Двусторонняя поверхность атаки

Brotli доходит до декодера с любой стороны HTTP-обмена - и именно это превращает библиотечный баг в широкую угрозу для деплоя.[1][3]

Сторона (a) - клиент атакует ваш сервер. Node.js-сервис, декодирующий Brotli-тело запроса, достижим напрямую. Это происходит двумя обычными путями. Во-первых, body-парсеры Express 5 (express.json(), express.urlencoded(), express.text(), express.raw()) и body-parser 2.x декодируют тело запроса с Content-Encoding: br, когда применяются к маршруту. Во-вторых, любой код, явно вызывающий Brotli-API node:zlib (zlib.brotliDecompress(), zlib.createBrotliDecompress()), разжимает то, что ему дали.[1][3] Обычный Node HTTP-сервер не декодирует Brotli-тело запроса просто потому, что получил его - для декодирования нужен middleware или явный вызов, так что экспонирование зависит от того, что ваш стек реально разжимает.[1]

Сторона (b) - вредоносный сервер атакует ваш клиент. Встроенный fetch() в Node и Bun, а также curl --compressed прозрачно декодируют ответ с Content-Encoding: br. Значит, сервис, с которым вы общаетесь (API, origin CDN, даже рекламный вендор), может вернуть специально собранный Brotli и, если рантайм не пропатчен, выполнить код в вашем процессе. Эту сторону пропускают чаще всего: чтобы быть затронутым, вам не обязательно принимать загрузки - достаточно сделать fetch чего-то недоверенного.[1][3]

Исследователи Imperva (они дали уязвимости имя Node2Shell и опубликовали proof of concept 22 октября) продемонстрировали реальное удалённое выполнение кода против официального Docker-образа Node.js 24.22.0 Linux arm64, без отладчика и без кастомной сборки. Они передали сфабрикованный Brotli-пейлоад либо в теле запроса, обработанном Express/body-parser, либо в ответе, поглощённом fetch(), и на диске появился marker-файл, хотя в приложении не было кода, способного его создать. Те же входы были отклонены на 24.22.1 - код не выполнился.[3][12][13]

Две честные оговорки из статьи Imperva: PoC срабатывал не с каждой попытки (выполнение требовало нескольких попыток в их Linux arm64-окружении), и тестировалась одна архитектура/сборка. По состоянию на 25 октября эксплойтов in-the-wild не сообщалось, и CVE ещё не было в каталоге CISA KEV.[13][14] Ни то ни другое не меняет действия: это memory-corruption баг в компоненте, который по замыслу декодирует недоверенные сетевые данные, и фикс - рутинный, но обязательный подъём версии.[4]

Проверяем свой VPS

Практическая часть для self-hosted машины. Выполните на каждом хосте, затем в каждом образе контейнера, который вы собираете.

Node.js - проверяем встроенную версию Brotli:

node -p process.versions.brotli

На актуальном рантайме вы должны увидеть 1.2.1 (или новее / пропатченное значение ветки). Если у вас Node 22.x / 24.x / 26.x и process.versions.brotli сообщает значение из уязвимого диапазона, или ваш Node ниже 22.23.4 / 24.22.1 / 26.12.1, - вы в зоне риска. Проверяйте внутри образа, который реально запускаете, а не только на хосте: docker run --rm <ваш-образ> node -p process.versions.brotli.[1][2]

Bun:

bun --version   # нужно 1.4.4 или новее
bun -e 'console.log(process.versions.brotli)'

Python:

pip list | grep -i brotli
# или
pip show brotli brotlicffi

Вам нужны brotli >= 1.2.1 и brotlicffi >= 1.2.1.0.[4] Вот тихая опасность в Python: популярные HTTP-клиенты - requests, urllib3, httpx, aiohttp - декодируют ответы с Content-Encoding: br автоматически, как только установлен brotli или brotlicffi, даже если ваш собственный код их не импортирует. На многих системах один из этих пакетов подтягивается транзитивно другой зависимостью, так что pip list | grep brotli - честная проверка: если любой из пакетов есть, ваши клиенты декодируют Brotli независимо от того, говорит ли об этом ваш код. При апгрейде пересоберите образы контейнеров - образ, собранный на уязвимом wheel, останется уязвимым до следующей сборки.[4]

Proxy / mesh рантаймы: для Envoy вы затронуты, только когда декомпрессор Brotli реально настроен; адвизори фиксит 1.36.12, 1.37.8, 1.38.6, 1.39.3, 1.40.0 (1.36 - end-of-life и не поддерживается).[5] Для Istio фикс пришёл в 1.31.2 / 1.30.6 и достаётся через обновление Envoy - обновите control plane и дайте sidecar’ам раскатиться.[7][8]

А что с nginx и вашим reverse-proxy стеком?

Ваш nginx reverse proxy почти наверняка не является здесь уязвимым декодером. Open-source nginx (и модуль nginx ngx_brotli, и сборка F5 NGINX Plus) использует Brotli для сжатия ответов на лету - то есть энкодер - и для раздачи заранее сжатых файлов; nginx по умолчанию не декодирует входящее тело запроса с Content-Encoding: br так, как это делает прикладной слой.[15][16] Поэтому реальные точки принятия решения на стеке nginx + Node/Python находятся за прокси:

  • Приложение на Node/Python, которое разжимает тела запросов (Express/body-parser, Python-фреймворки) или делает fetch() во внешние сервисы - здесь работает декодер, и именно здесь надо патчить.
  • Клиенты, которые вы запускаете с машины: curl --compressed, Node/Bun-скрипты, обращающиеся к API. На пропатченном рантайме они безопасны; на непропатченном вы затронуты каждым сервером, с которым общаетесь.

Честная рамка для self-hosted аудитории: reverse proxy - не ваша стена от этой CVE. Он не декодирует тела запросов, так что блокировка br на nginx (срезав или игнорируя Content-Encoding: br до попадания в приложение, или ограничив Accept-Encoding на проксируемых ответах) может сократить поверхность, но ничего не делает для исходящей стороны fetch(), да и балансировщик сам по себе не декодер. Фикс живёт в рантаймах за ним. Если вы правильно настроили TLS и заголовки прокси - это уже есть в нашем гайде по укреплённому nginx reverse proxy - но рантаймы остаются недостающим звеном.[17]

Что касается механики апгрейда: если у вас Node под PM2, последовательность перезапуска та же, что описана в гайде по деплою Next.js на VPS: обновите рантайм, пересоберите standalone-артефакт, перезапустите каждый PM2-процесс - работающий процесс держит старый код в памяти, пока вы его не перезапустите. EOL-ветки вне поддержки (Node 20 и младше) фикса не получают и workaround’а в рантайме не имеют, так что единственный путь оттуда - переход на поддерживаемую ветку.[1][3]

Порядок обновления

  1. Сначала Node-подобные рантаймы - всё, что декодирует недоверенный Brotli из сети и держит важные данные: Node до 22.23.4 / 24.22.1 / 26.12.1, Bun до 1.4.4.[1][6]
  2. Python - обновите brotli >= 1.2.1, brotlicffi >= 1.2.1.0, затем пересоберите образы, чтобы wheel’ы реально сменились.[4]
  3. Proxy / mesh - Envoy до фиксированного релиза (≥ 1.40.0 / 1.39.3 / 1.38.6 / 1.37.8), Istio до 1.31.2 / 1.30.6, когда включена декомпрессия Brotli.[5][7][8]
  4. Пересоберите каждый контейнер, где встроен Node/Bun/Python-рантайм - важен бинарник внутри образа, а инвентаризация пакетов может пропустить библиотеку Brotli, запечённую в слой.[3]
  5. Перепроверьте после перезапуска командами выше, внутри работающего контейнера, и убедитесь, что process.versions.brotli / pip list выглядят правильно.

Workaround’а внутри Node.js нет - адвизори его не указывает, так что приложение, которое не может обновиться, не имеет отступления в рантайме, кроме ограничения недоверенного Brotli-трафика на затронутых путях до прихода апгрейда.[1][12] Фикс, закрывающий это для всех, на любом слое, - один и тот же ответ из двух слов: обновитесь. Ваш VPS сам себя не патчит - это работа, которую за кого-то делает облачная платформа, и та, которую вы взяли на себя, перейдя на self-hosting.

Источники

  1. Node.js - “Wednesday, October 21, 2026 Security Releases” (ветки 26.x/24.x/22.x; максимальная severity HIGH; EOL-ветки всегда затронуты security-релизом). https://nodejs.org/en/blog/vulnerability/october-2026-security-releases - checked 2026-10-25
  2. GitHub Advisory GHSA-9v8h-qqgm-4m35 - “Brotli: Out-of-bounds write in the Brotli decoder (‘Node2Shell’) also affects the PyPI Brotli package” (Critical, 9.2 CVSS 4.0; затронут Google Brotli 1.0.3–1.2.0; исправлено brotli 1.2.1 / brotlicffi 1.2.1.0, ссылаясь на brotli 1.0.10/1.1.1/1.2.1). https://github.com/advisories/GHSA-9v8h-qqgm-4m35 - checked 2026-10-25
  3. Imperva - “Node2Shell: From Brotli Data to Remote Code Execution” (RCE на официальном образе Node.js 24.22.0 Linux arm64; фиксы 22.23.4/24.22.1/26.12.1; тестировались Express 5.0.0–5.3.0 и body-parser 2.0.0–2.3.0; апстрим-патч google/brotli 0ed4268, фикс в исходниках 27 февраля, релиз 21 октября). https://www.imperva.com/blog/node2shell-from-brotli-data-to-remote-code-execution/ - checked 2026-10-25
  4. GitHub Advisory GHSA-9v8h-qqgm-4m35 (Python-пакеты) - затронуты brotli ≥1.0.3 <1.2.1 / brotlicffi <1.2.1.0; исправлено 1.2.1 / 1.2.1.0; HTTP-клиенты (requests/urllib3/httpx/aiohttp) декодируют br автоматически при наличии пакета. https://github.com/advisories/GHSA-9v8h-qqgm-4m35 ; https://github.com/pypa/advisory-database/tree/main/vulns/brotli/PYSEC-2026-4322.yaml ; https://github.com/pypa/advisory-database/tree/main/vulns/brotlicffi/PYSEC-2026-4323.yaml - checked 2026-10-25
  5. Envoy - “Out-of-Bounds Write in Brotli Decompressor” GHSA-3wvj-j6qm-62mr (Critical; затронуты v1.39.2/v1.38.5/v1.37.7/v1.36.11; исправлено v1.40.0/v1.39.3/v1.38.6/v1.37.8/v1.36.12; экспонирование только при настроенном декомпрессоре Brotli). https://github.com/envoyproxy/envoy/security/advisories/GHSA-3wvj-j6qm-62mr - checked 2026-10-25
  6. Bun - релиз v1.4.4 (22 октября 2026; встраивает Brotli 1.2.1; security-фиксы TLS/WebSocket/клиентов БД/пакетного менеджера, включая Brotli; фикс регрессии против 1.4.3). https://bun.com/blog/bun-v1.4.4 ; GHSA-663w-cv8g-jj2p https://github.com/oven-sh/bun/security/advisories/GHSA-663w-cv8g-jj2p - checked 2026-10-25
  7. Istio - ISTIO-SECURITY-2026-007 (Envoy CVE-2026-111497; пользователи, включившие декомпрессию Brotli через EnvoyFilter, должны обновиться). https://istio.io/latest/news/security/istio-security-2026-007/ - checked 2026-10-25
  8. Istio - “Announcing Istio 1.31.2” (security-обновление, включающее CVE Brotli в Envoy). https://istio.io/latest/news/releases/1.31.x/announcing-1.31.2/ - checked 2026-10-25
  9. Google Brotli releases - теги v1.0.10 / v1.1.1 / v1.2.1 с фиксом декодера. https://github.com/google/brotli/releases/tag/v1.2.1 - checked 2026-10-25
  10. NVD - CVE-2026-111497 (Google; CVSS 4.0 = 9.2; out-of-bounds write в декодере Brotli; CWE-252 / CWE-787; удалённый неаутентифицированный). https://nvd.nist.gov/vuln/detail/CVE-2026-111497 - checked 2026-10-25
  11. Raven - “Node2Shell: Node.js RCE Risk in CVE-2026-111497” (разбор первопричины: DecodeBlockTypeAndLength / BROTLI_SAFE / устаревший block_type_rb[3]; апстрим-фикс 0ed4268 меняет возврат на BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH). https://raven.io/blog/node2shell-cve-2026-111497-nodejs-brotli - checked 2026-10-25
  12. The Hacker News - “Researchers Demonstrate Node2Shell RCE Exploit on Node.js Build Using Crafted Brotli Data” (PoC Imperva на Node 24.22.0 arm64; body-парсеры Express 5 декодируют br-тело запроса; в рантайме workaround не указан). https://thehackernews.com/2026/10/researchers-demonstrate-node2shell-rce.html - checked 2026-10-25
  13. The Hacker News - “Brotli Decoder Flaw Behind Node.js RCE Also Affects Bun, Python and Other Widely Used Software” (таблица затронутости Envoy/Istio; нет эксплойтов in-the-wild, не в CISA KEV по состоянию ~23–25 октября; фикс в исходниках датирован 27 февраля). https://thehackernews.com/2026/10/brotli-decoder-flaw-behind-nodejs-rce.html - checked 2026-10-25
  14. Cycode - “node2shell (CVE-2026-111497): What to Patch in Node.js” (по состоянию на 23 октября нет эксплойтов in-the-wild, не в CISA KEV; фикс Node 22.23.4/24.22.1/26.12.1, Bun 1.4.4). https://cycode.com/blog/node2shell-cve-2026-111497 - checked 2026-10-25
  15. NGINX - динамический модуль Brotli (ngx_http_brotli filter = сжатие ответов; static = предсжатые файлы; NGINX Plus). https://docs.nginx.com/nginx/admin-guide/dynamic-modules/brotli/ - checked 2026-10-25
  16. google/ngx_brotli - набор модулей: ngx_http_brotli_filter_module (сжимает ответы) и ngx_http_brotli_static_module (раздаёт предсжатые); декомпрессии входящих тел запросов нет. https://github.com/google/ngx_brotli - checked 2026-10-25
  17. DeploySmith - гайд по укреплённому nginx reverse proxy (TLS, заголовки, гигиена прокси). https://deploysmith.dev/ru/guides/nginx-reverse-proxy-config/ - checked 2026-10-25