Выбор языка с учётом безопасности на примере HTTP2Shell

12 min

language: ja bn en es hi pt ru zh-cn zh-tw

Здравствуйте, я Мунō.

Недавно в декодере Brotli обнаружили уязвимость, которую, похоже, называют Node2Shell, HTTP2Shell и другими похожими именами.

Речь о CVE-2026-111497: при распаковке некорректных данных, сжатых Brotli, возникает запись за пределами буфера, что может привести к аварийному завершению процесса, утечке содержимого памяти, а при определённых условиях — к выполнению произвольного кода.

Иными словами, поскольку проблема находится в реализации декодера, обновлять нужно и HTTP-клиенты. Если на серверной стороне включён Brotli, думаю, тоже стоит проверить необходимость обновления, но по масштабу воздействия клиентская сторона HTTP, на мой взгляд, затронута сильнее.

Однако в этот раз речь не столько о самой уязвимости, сколько о том, что при возникновении подобных проблем удобство доставки исправлений довольно сильно зависит от выбранного языка и организации библиотечных зависимостей.

Обновить библиотеку ОС — не всегда достаточно

Например, если на клиентской стороне libbrotli используется как разделяемая библиотека ОС, то обычно достаточно установить исправленную версию через пакетный менеджер соответствующей ОС. Разумеется, при условии, что дистрибутив уже предоставил исправление; кроме того, запущенные процессы иногда требуется перезапустить.

Но это справедливо лишь в том случае, если приложение использует динамическую компоновку с разделяемой библиотекой ОС.

Даже если используется тот же Brotli, но при сборке библиотека статически скомпонована с исполняемым файлом, обновление libbrotli в ОС не изменит содержимое этого файла. Придётся либо ждать исправленной версии от разработчика, либо самостоятельно обновлять зависимости и пересобирать приложение.

То есть важно не просто то, «на каком языке это написано», а то, «где находится проблемный код и чьё обновление его заменит».

Rust удобен для распространения бинарных файлов, но есть и обратная сторона

В Rust обычные крейты, написанные на Rust, часто включаются непосредственно в исполняемый файл, благодаря чему приложение удобно распространять в виде одного бинарного файла. Мне самому эта простота действительно очень нравится.

С другой стороны, если в коде зависимости обнаруживается уязвимость, одного обновления пакетов ОС в некоторых случаях будет недостаточно. Чтобы исправить код зависимости, включённый в бинарный файл, как правило, необходимо пересобрать и заново распространить приложение.

В случае с Brotli ситуация тоже различается для обёрток вроде brotli2, вызывающих реализацию на C из Rust, и библиотек вроде brotli, реализованных на Rust. Более того, даже при использовании реализации на C способ обновления зависит от того, подключается ли разделяемая библиотека динамически или код на C тоже включается статически. Нельзя утверждать, что программа обязательно подвержена этой уязвимости в реализации на C лишь потому, что она написана на Rust.

В конце концов, безопасность памяти в Rust и удобство обновления зависимостей — разные вопросы.

Я считаю Rust очень полезным для разработки библиотек и компонентов, где важна производительность. Но при использовании его для приложений, рассчитанных на широкое распространение, нужно продумывать и доставку обновлений безопасности после выпуска. Обратная сторона возможности создавать быстрые и самодостаточные бинарные файлы заключается в том, что ответственность за доставку исправлений пользователям в большей степени ложится на само приложение.

В Node.js удобно то, что можно обновить среду выполнения

Node.js тоже затронут этой уязвимостью. Поскольку реализация Brotli встроена в сам Node.js, одного обновления libbrotli в ОС недостаточно: необходимо обновить Node.js до исправленной версии.

Для этого случая в рамках релизов безопасности Node.js выпущены версии 22.23.4, 24.22.1 и 26.12.1.

При этом для обычного приложения на Node.js достаточно обновить среду выполнения Node.js и перезапустить приложение, чтобы начать использовать исправленную реализацию Brotli, входящую в её состав. Конечно, случаи, когда npm-пакет содержит другую уязвимую реализацию или сам Node.js включён в исполняемый файл приложения, — отдельная история.

В этом отношении эксплуатация в некотором смысле кажется мне проще, чем распространение отдельных бинарных файлов Rust. Ведь иногда проблему можно решить обновлением общей среды выполнения, не заставляя каждого пользователя пересобирать множество приложений.

Впрочем, само по себе использование Node.js не означает, что уязвимость сработает. Проблема возникает, если существует путь обработки, на котором распаковываются некорректные данные Brotli. По моим ощущениям, реализаций HTTP-серверов, специально распаковывающих входящие запросы с помощью Brotli, не так много. Однако fetch(), который обращается к внешним ресурсам в роли клиента, автоматически распаковывает ответы, сжатые Brotli, поэтому проверять только ту часть сервера, которая слушает входящие соединения, недостаточно.

Раньше я скорее был противником Node.js, но, глядя на то, насколько богаче в последнее время стали его стандартные API, я заметно изменил своё мнение. fetch(), Web Streams, средство запуска тестов — многое из того, для чего раньше требовались сторонние пакеты, теперь в значительной степени покрывается самим Node.js.

Чем меньше зависимостей от пакетов, тем меньше объектов, обновления которых приходится контролировать. По сравнению с прошлым Node.js кажется мне гораздо более привлекательным вариантом для разработки приложений.

Всё-таки Go, на мой взгляд, очень хорошо спроектирован

В этом отношении Go кажется мне языком, который проектировали с явным прицелом на разработку приложений.

Его стандартная библиотека, включая net/http, практична: при создании HTTP-клиентов и серверов большую часть функциональности часто можно реализовать стандартными пакетами, не привлекая множество внешних зависимостей. Стандартный HTTP-клиент автоматически распаковывает в основном gzip и по умолчанию не использует тот самый декодер Google Brotli на C, о котором идёт речь.

Конечно, в Go тоже обычно включают код зависимостей в бинарный файл, поэтому при обнаружении уязвимости в этом коде потребуется пересборка и повторное распространение. Сам по себе выбор Go не означает, что можно обойтись обновлением библиотек ОС. А если использовать внешние библиотеки через cgo, вопрос динамической или статической компоновки возникает точно так же.

И всё же я считаю, что широкий круг задач, решаемых одними стандартными пакетами, и возможность легко сохранять небольшое число необходимых зависимостей — серьёзное преимущество не только с точки зрения сопровождения, но и с точки зрения безопасности.

При выборе языка стоит учитывать не только производительность выполнения

Я вовсе не хочу сказать, что Rust плох, а Node.js или Go безопасны. Эта уязвимость находится в реализации Brotli на C, и по одному лишь названию языка нельзя определить, затронуто ли приложение.

Но при выборе языка, на мой взгляд, стоит оценивать не только производительность, потребление памяти и эффективность разработки, но и то, кто в конечном счёте и каким образом будет доставлять пользователям обновления зависимых библиотек.

В случае разделяемых библиотек иногда можно исправить всё разом обновлением пакетов ОС. Статически скомпонованный бинарный файл, с другой стороны, самодостаточен и включает код зависимостей, но для доставки исправлений приходится обновлять каждое приложение. Подход со средой выполнения, как у Node.js, тоже имеет преимущество в виде возможности централизованно обновить общую среду, однако уязвимость в самой среде выполнения может затронуть широкий круг приложений.

В конечном счёте это вопрос проектирования: что и куда включать, как распространять и за что брать на себя ответственность.

Лично я по-прежнему хочу активно использовать Rust для создания библиотек и компонентов, которым нужна высокая производительность. Но случай с HTTP2Shell вновь напомнил мне, что для обычных приложений тоже есть немало веских причин выбирать Go или Node.js.

Related Posts