Choosing a Language with Security in Mind: A Perspective from HTTP2Shell

11 min

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

Hello, I'm Munou.

A vulnerability was recently discovered in the Brotli decoder, and it seems to be going by names such as Node2Shell and HTTP2Shell.

This is CVE-2026-111497: decompressing malformed Brotli-compressed data can cause an out-of-bounds write, potentially leading to a process crash, disclosure of memory contents, or, under certain conditions, arbitrary code execution.

In other words, because this is an issue with the decoder implementation, HTTP clients also need updating. If you have Brotli enabled on the server side, I think you will need to address updates there too, but in terms of impact, HTTP clients seem to be more significantly affected.

This time, rather than discussing the vulnerability itself, I want to talk about how, when problems like this arise, the language and library setup you have chosen can make quite a difference to how easily fixes can be delivered.

Updating the OS library is not necessarily enough

For example, if a client uses libbrotli as an OS shared library, you can generally address the issue by installing a patched version through the OS package manager. Of course, this assumes that the distribution has provided a fix, and running processes may need to be restarted.

However, this applies when the application is dynamically linked to the OS shared library.

Even if it uses the same Brotli, if the library is statically linked into the executable at build time, updating the OS's libbrotli will not change what is inside the binary. You need to wait for the developer to distribute a patched version, or update the dependencies and rebuild it yourself.

In other words, what matters is not simply which language it is written in, but where the problematic code resides and whose update will replace it.

Rust makes distributing binaries convenient, but there is a trade-off

With Rust, ordinary crates written in Rust are often incorporated into the executable, making it convenient to distribute applications as standalone binaries. I actually think that convenience is a major advantage.

On the other hand, if a vulnerability is discovered in a dependency, there are cases where updating only the OS packages will not fix it. Fixing dependency code included in a binary generally requires rebuilding and redistributing the application.

With this Brotli issue, too, the situation differs between wrappers such as brotli2, which call the C implementation from Rust, and libraries such as brotli, which are implemented in Rust. Even when using the C implementation, the update procedure depends on whether it is dynamically linked as a shared library or whether the C code is statically linked as well. Being written in Rust does not automatically mean an application is affected by this vulnerability in the C implementation.

Fundamentally, Rust's memory safety and the ease of updating dependency code are separate issues.

While I think Rust is extremely useful for library development and performance-critical components, using it for widely distributed applications requires thinking about security updates after distribution as well. The flip side of being able to create fast, self-contained binaries is that more of the responsibility for getting fixes to users falls on the application side.

With Node.js, being able to update the runtime makes things easier

Node.js is also affected by this issue. Because the Brotli implementation is built into Node.js itself, simply updating the OS's libbrotli does not resolve it; you need to update to a patched version of Node.js.

For this issue, versions 22.23.4, 24.22.1, and 26.12.1 have been published in the Node.js security releases.

For a typical Node.js application, though, updating the Node.js runtime and restarting the application applies the fix to the Brotli implementation included in it. Of course, it is a different story if an npm package includes another vulnerable implementation, or if Node.js itself is bundled into the executable.

In a sense, I feel this is easier to manage operationally than distributing standalone Rust binaries. In some cases, a shared runtime update can address the issue without requiring users to rebuild large numbers of applications individually.

That said, merely using Node.js does not trigger this vulnerability. It becomes an issue when there is a code path that decompresses malformed Brotli data. My impression is that relatively few HTTP servers explicitly decompress incoming requests with Brotli. On the other hand, fetch(), which accesses external services as a client, automatically decompresses Brotli-compressed responses, so looking only at the server's listening side is not enough.

I used to be rather anti-Node.js, but seeing how comprehensive its standard APIs have become lately has changed my impression quite a bit. With fetch(), Web Streams, the test runner, and more, Node.js itself can now cover much of what previously required external packages.

The fewer dependency packages you have, the fewer things you need to keep updated. Compared with the past, I now have a much more favorable impression of Node.js as an option for writing applications.

I still think Go is very well designed

In that respect, I feel that Go is a language designed with a strong focus on application development.

Its standard library, including net/http, is practical, and you can often implement most of an HTTP client or server using only standard packages, without bringing in a large number of external dependencies. The standard HTTP client generally only decompresses gzip automatically; it does not use Google's Brotli C decoder by default.

Of course, Go applications also commonly incorporate dependency code into their binaries, so if that code has a vulnerability, rebuilding and redistributing is necessary. Using Go does not mean updating OS libraries alone is enough, and if you use external libraries through cgo, the same questions of dynamic versus static linking arise.

Even so, I think the broad range of things you can accomplish using only standard packages, and the ease of keeping necessary dependencies to a minimum, are significant advantages not just for maintainability but also for security.

Execution performance is not the only thing to consider when choosing a language

I am not saying that Rust is bad, or that Node.js and Go are safe. This vulnerability is in Brotli's C implementation, so you cannot determine whether something is affected based on the language name alone.

However, when choosing a language, I think you should evaluate not only execution performance, memory usage, and development efficiency, but also who ultimately delivers dependency library updates to users, and how.

With shared libraries, an OS package update may be able to fix everything at once. Statically linked binaries, on the other hand, can be self-contained, dependencies included, but each application needs to be updated when a fix is required. Runtime-based environments such as Node.js offer the advantage of being able to update a shared runtime, but a vulnerability in the runtime itself can have a broad impact.

Ultimately, this is a design question about what you incorporate where, how you distribute it, and how much responsibility you take on.

Personally, while I want to make active use of Rust for building libraries and performance-critical components, seeing this HTTP2Shell issue has reminded me that there are also compelling reasons to choose Go or Node.js for general-purpose applications.

Related Posts