HTTP2Shellの観点から見たセキュリティも加味した言語選定

5 min

こんにちは、無能です。

先日、Brotliのデコーダに脆弱性が見つかり、Node2ShellやHTTP2Shellなどと呼ばれているようです。

CVE-2026-111497 のことで、不正なBrotli圧縮データを展開すると境界外書き込みが発生し、プロセスのクラッシュやメモリ情報の漏えい、条件によっては任意コード実行につながる可能性があるというもの。

つまりはデコーダ側の実装の為、HTTPクライアント側も更新が必要でありサーバーサイド側で Brotli を有効化している場合、更新対応必要かと思いますが影響度合いで言えばHTTPクライアント側の方が大きいように思います。

今回は脆弱性そのものよりも、こういった問題が出たときに、採用した言語やライブラリの構成によって修正の届けやすさが結構変わってくるよなあ、という話です。

OSのライブラリを更新すれば終わり、とは限らない

例えばクライアント側でOSの共有ライブラリとして libbrotli を利用しているのであれば、基本的には各OSのパッケージマネージャーから修正版を導入することで対応できます。もちろんディストリビューション側で修正が提供されていることが前提ですし、実行中のプロセスは再起動が必要になる場合があります。

ただし、これはアプリケーションがOSの共有ライブラリを動的リンクしている場合の話。

同じBrotliを使っていても、ビルド時にライブラリを実行ファイルへ静的リンクしていれば、OS側の libbrotli を更新してもバイナリの中身は変わりません。開発元から修正版が配布されるのを待つか、自分で依存関係を更新してビルドし直す必要があります。

つまり重要なのは、単に「この言語で書かれているか」ではなく、「問題のあるコードがどこに入っていて、誰の更新によって置き換わるか」という部分です。

Rustはバイナリ配布が便利な一方で

Rustの場合、通常のRust製crateは実行ファイルへ組み込まれることが多く、単体バイナリとして配布しやすいのが便利です。実際、自分もその手軽さはかなり良いと思っています。

一方で、依存しているコードに脆弱性が見つかった場合、OSのパッケージだけを更新しても修正されないケースが出てきます。バイナリに含まれた依存コードの修正には、原則としてアプリケーションの再ビルドと再配布が必要になります。

今回のBrotliでも、RustからC実装を呼び出す brotli2 のようなラッパーと、Rustで実装された brotli のようなライブラリとでは事情が異なります。さらにC実装を使っていても、共有ライブラリを動的リンクするのか、Cのコードまで静的リンクするのかで更新方法が変わります。Rust製だから今回のC実装の脆弱性に必ず該当する、という話ではありません。

そもそもRustのメモリ安全性と、依存コードの更新のしやすさは別の問題です。

Rustはライブラリ開発や性能が重要な部分では非常に有益だと思う反面、広く配布するアプリケーションで使う場合は、配布後のセキュリティアップデートまで含めて考えないといけない。高速で自己完結したバイナリを作れるメリットの裏返しとして、修正を利用者へ届ける責任もアプリケーション側に寄ってくるわけです。

Node.jsはランタイムを更新できるのが楽

Node.jsも今回の影響を受けています。Brotliの実装がNode.js本体に組み込まれているため、OSの 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圧縮されたレスポンスを自動で展開するため、サーバーのListen側だけ見ていればいいという話でもありません。

昔はどちらかというとNode.jsアンチだったのですが、最近の標準APIの充実ぶりを見ると結構印象が変わってきています。fetch() やWeb Streams、テストランナーなど、以前なら外部パッケージに頼っていた部分もNode.js本体だけでかなりカバーできるようになりました。

依存パッケージを減らせれば、それだけ管理する更新対象も減らせる。Node.jsは昔に比べて、アプリケーションを書くための選択肢としてかなり好感触です。

Goの設計はやっぱりよくできていると思う

その点、Goはアプリケーション開発をかなり意識して設計されている言語だなあと思います。

net/http をはじめ標準ライブラリが実用的で、HTTPクライアントやサーバーを作るために大量の外部依存を持ち込まなくても標準パッケージだけで大半の実装が済むことが多い。標準のHTTPクライアントが自動的に展開するのは基本的にgzipであり、今回のGoogle BrotliのCデコーダを標準で利用しているわけでもありません。

もちろんGoも依存コードをバイナリに組み込む構成が一般的なので、そのコードに脆弱性があれば再ビルドと再配布が必要です。GoだからOSのライブラリ更新だけで済むということではないし、cgo経由で外部ライブラリを使えば、動的リンクか静的リンクかの問題もそのまま出てきます。

それでも、標準パッケージだけで完結できる範囲が広く、必要な依存を少なく保ちやすいというのは、保守性だけでなくセキュリティ面でもかなり大きいと思っています。

言語選定で見るべきなのは実行性能だけではない

別にRustが悪いとか、Node.jsやGoなら安全という話ではありません。そもそも今回の脆弱性はBrotliのC実装にあるもので、言語の名前だけで影響の有無は判断できません。

ただ、言語を選ぶ際には実行性能やメモリ使用量、開発効率だけでなく、依存ライブラリの更新を最終的に誰が、どうやって利用者へ反映するのかも評価した方がいいと思います。

共有ライブラリならOSのパッケージ更新でまとめて直せる可能性がある。一方で静的リンクされたバイナリは依存コードを含めて自己完結できるものの、修正時にはアプリケーションごとに更新が必要になる。Node.jsのようなランタイム型も、本体を共通で更新できる利点がある反面、ランタイム自体に脆弱性が出れば広範囲に影響します。

結局は何をどこに組み込むか、どう配布するか、どこまで面倒を見るかという設計の話です。

個人的にはRustはライブラリを作る用途や性能が必要なコンポーネントで積極的に使いたい一方で、一般的なアプリケーションならGoやNode.jsを選ぶ理由も結構大きいなあと、今回のHTTP2Shellを見て改めて感じました。

Related Posts