[解説] browserslistに発覚したメモリ肥大化脆弱性 (GHSA-c83g-rgw3-j3cx) - フロントエンド開発への影響
はじめに:あなたのプロジェクトは大丈夫? browserslistの役割と重要性
`browserslist`は、ターゲットブラウザのリストを定義するための重要なツールであり、Autoprefixer, Babel, PostCSS, webpack, Next.jsなど、多くの人気のあるフロントエンドツールやビルドシステムで間接的・直接的に利用されています。プロジェクトの互換性維持に不可欠なこのライブラリに、メモリ肥大化の脆弱性が発見されました。今回は、この深刻な脆弱性「GHSA-c83g-rgw3-j3cx」(CVE-2026-73089)について、技術的な詳細とフロントエンド開発への影響、そして取るべき対策を解説します。
脆弱性の概要:なぜメモリは増え続けるのか?
この脆弱性は、`browserslist`ライブラリの内部キャッシュ機構に起因します。具体的には、`browserslist()`関数が結果をキャッシュする`cache`オブジェクトと、クエリのASTをキャッシュする`parseCache`オブジェクトが、一度格納されたエントリを無制限に保持し続ける設計になっていたことが根本原因です。深刻度は高いと評価されています。
通常のキャッシュであれば、一定のサイズ制限や有効期限(TTL)が設けられますが、`browserslist`のキャッシュにはこれらが欠けていました。結果として、異なるクエリが呼び出されるたびに新しいエントリが追加され続け、メモリを永続的に消費し続けます。
技術的な詳細:無限キャッシュと「Since」クエリの悪用
問題のコードは、`index.js`内の`cache`と`parseCache`の初期化部分に見られます。これらの変数は、単なるJavaScriptのオブジェクトとして定義され、新しいキーが追加されると、既存のキーが削除されることなく無限に拡張されていきます。以下は脆弱性のあるコードの抜粋です。
```javascript var cache = {} var parseCache = {} function browserslist(queries, opts) { ... var cacheKey = JSON.stringify([queries, context]) if (cache[cacheKey]) return cache[cacheKey] ... // ここでcacheKeyとresultが永続的に保存される if (!env.env.BROWSERSLIST_DISABLE_CACHE) { cache[cacheKey] = result } return result } function parseQueries(queries) { var cacheKey = JSON.stringify(queries) if (cacheKey in parseCache) return parseCache[cacheKey] var result = parseWithoutCache(QUERIES, queries) // ここでcacheKeyとresultが永続的に保存される if (!env.env.BROWSERSLIST_DISABLE_CACHE) { parseCache[cacheKey] = result } ... } ```
特に悪用されやすいのが、`since <year>-<month>-<day>`形式のクエリです。このクエリは、日付の組み合わせをほぼ無限に受け入れるため、攻撃者はわずか約17バイトの異なるキャッシュキーを大量に生成できます。それぞれのキーは、約8.5KBにもなるブラウザリストの結果をキャッシュするため、非常に効率的にメモリを消費させることが可能です。
実際の測定では、20,000個の異なる`since`クエリ(合計330KBの入力)で、**50MB以上**のヒープメモリが永続的に保持され、**約150倍**のメモリ増幅が確認されています。このメモリ消費は、クエリ数に比例して上限なく増加し続けました。
攻撃シナリオと影響:DoSの脅威
この脆弱性は、特に以下のような長期間稼働するプロセスに大きな影響を与えます。
<ul><li>**Webサーバー**: ユーザーからのリクエストに基づいて`browserslist()`を呼び出すサーバー(例: SSRアプリケーションのビルドプロセスなど)。</li><li>**CI/CDワーカー**: ビルドやテストのたびに`browserslist`を利用する、ウォームアップされたCI/CD環境。</li><li>**デーモンプロセス**: バックグラウンドで`browserslist`を利用するサービス。</li></ul>
攻撃者は、外部入力によって影響されるクエリ値(例: `since 1900-01-01`, `since 1900-01-02`など)を大量に生成し、異なるリクエストを通じてこれらを送信します。プロセスはこれらのクエリ結果をキャッシュし続けるため、最終的にはメモリを使い果たし、クラッシュ(サービス停止)に至ります。これは単一のリクエストで発生するDoS攻撃とは異なり、時間経過とともに蓄積される「ボリューム型攻撃」である点が特徴です。
推奨される対策:LRUキャッシュへの移行
開発チームは、この問題を解決するために、単純なJavaScriptオブジェクトだったキャッシュを、サイズ制限のある`Map`ベースのLRU(Least Recently Used:最も長い間使われていないものから削除)キャッシュに置き換える修正を実装しました。
新しいキャッシュ機構では、`CACHE_MAX_ENTRIES`という最大エントリ数が設定され、キャッシュがいっぱいになると、最も古いエントリが自動的に削除されるようになります。以下は修正後のコードの抜粋です。
```javascript var CACHE_MAX_ENTRIES = 500 // 例:最大500エントリ function boundedCacheSet(map, key, value) { if (map.size >= CACHE_MAX_ENTRIES) { // 最も古いエントリを削除 (Mapは挿入順を保持するため) map.delete(map.keys().next().value) } map.set(key, value) } var cache = new Map() var parseCache = new Map() // ... 以降、read/writeがboundedCacheSet()に変更される ```
この修正により、メモリ消費は大幅に改善されました。修正後のテストでは、5,000から40,000の異なる`since`クエリを処理しても、ヒープメモリは常に約4.9MBで横ばいとなり、以前の最大52.3MBから劇的に削減されたことが確認されています。
影響範囲の確認といますぐ実施すべき対応
自身のプロジェクトで`browserslist`を直接的または間接的に使用している場合(多くのフロントエンドプロジェクトが該当します)、この脆弱性の影響を受ける可能性があります。特に、外部からの入力を受け付け、それに基づいて`browserslist`クエリを動的に生成・実行するような長期間稼働するシステムを運用している場合は、速やかな対応が求められます。
**推奨される対応は、`browserslist`を最新バージョンにアップデートすることです。** この脆弱性はすでに修正済みであり、最新バージョンに更新するだけでリスクを回避できます。
アップデートがすぐに困難な場合は、環境変数`BROWSERSLIST_DISABLE_CACHE`を`true`に設定することで、キャッシュ機構自体を無効化できます。ただし、これは`browserslist`のパフォーマンスに影響を与える可能性があるため、一時的な回避策として検討してください。
まとめ
`browserslist`のメモリ肥大化脆弱性は、長期間稼働するシステムにおいてサービス停止を引き起こす可能性がある、深刻な問題です。普段意識することの少ないビルドツールやライブラリの脆弱性ですが、サプライチェーン攻撃の観点からもその影響は無視できません。
皆さんのプロジェクトを安全に保つためにも、利用しているライブラリの依存関係を定期的に確認し、最新バージョンへのアップデートを怠らないようにしましょう。