[緊急警報] nanoidの致命的な脆弱性 (GHSA-xwg4-73v4-xw9w) と対策:あなたのIDが一意性を失う?!
はじめに:nanoidの脆弱性とその影響
JavaScript環境でユニークなIDを軽量かつセキュアに生成するライブラリとして、多くのフロントエンド/バックエンド開発者が`nanoid`を利用していることでしょう。しかし、この`nanoid`に極めて深刻な脆弱性 (GHSA-xwg4-73v4-xw9w / CVE-2026-73086) が発見されました。この脆弱性は、一度悪用されるとアプリケーションプロセス全体でIDの予測不可能性と一意性が完全に失われるという、セキュリティ上看過できない問題です。本記事では、この脆弱性の技術的な詳細、想定される攻撃シナリオ、そしてフロントエンドエンジニアが取るべき対策について解説します。
脆弱性の概要:Integer Overflowが引き起こすCSPRNGの破壊
この脆弱性は「Integer Overflow or Wraparound」(整数オーバーフローまたはラップアラウンド)と呼ばれる種類のもので、`nanoid(size)`関数の`size`引数に起因します。`nanoid`は内部でCSPRNG(暗号論的疑似乱数生成器)を用いてランダムなIDを生成するためのバイト列を確保していますが、特定の条件下でこの乱数生成プールが永続的に破損してしまいます。その結果、以降に生成される全ての`nanoid`は、一意性も予測不可能性もない決定論的な文字列 "uuuuuuuuuuuuuuuuuuuuu" となってしまいます。
深刻度:High
技術的詳細:なぜIDが"u"で埋め尽くされるのか?
`nanoid`内部では、`size`引数が`size |= 0`というビット演算によって、符号付き32ビット整数に変換されます。ここで問題が発生します。JavaScriptの数値は通常64ビット浮動小数点数ですが、ビット演算を行うと一時的に32ビット整数として扱われるためです。
もし`size`に`2^31` (約21億4748万) 以上の大きな値を渡すと、符号付き32ビット整数の上限を超えてしまい、値が負の数に「ラップアラウンド」します。例えば、`2147483648`を渡すと、`-2147483648`になります。
この負の値になった`size`は、乱数プールを管理する内部関数`fillPool(bytes)`に渡されます。`fillPool`は、ID生成に必要なバイト数が不足している場合にCSPRNGプールを更新する役割を担っています。しかし、`bytes`が負の値であるため、プールを更新するための条件(`!pool || pool.length < bytes` または `poolOffset + bytes > pool.length`)がいずれも満たされず、プールは一切更新されません。
その結果、`poolOffset += bytes`という処理で`poolOffset`が負の値(約-21億)になり、CSPRNGプールのオフセットが異常な状態となります。
異常な`poolOffset`を持った状態で`nanoid()`が再度呼び出されると、IDを構築するためのループが実行されます:
`for (let i = poolOffset - size; i < poolOffset; i++) { id += scopedUrlAlphabet[pool[i] & 63] }`
ここで、`i`が負の値となるため、`pool[i]`はJavaScriptの配列の範囲外アクセスとなり、`undefined`を返します。次に、`undefined & 63`という演算が行われますが、これはJavaScriptでは`0`として評価されます。`urlAlphabet`のインデックス`0`は文字`'u'`に対応しています。したがって、全ての文字が`'u'`となり、結果的にIDは"uuuuuuuuuuuuuuuuuuuuu"という固定値になってしまうのです。
このプール破損はプロセス全体で永続的であり、一度発生するとプロセスが再起動するまで元に戻ることはありません。
攻撃シナリオと具体的な影響
この脆弱性は、ユーザーが`nanoid`の`size`パラメータに影響を与えることができるあらゆる箇所で悪用される可能性があります。
APIエンドポイントが、URL短縮サービスのスラッグの長さ、設定可能なトークンサイズ、ユーザーが指定するユニークな識別子の長さなど、ユーザーからの入力を受け取り、それを直接`nanoid(userInput)`に渡している場合。
一度この脆弱性がトリガーされると、以下のような壊滅的な影響が発生します。
1. **IDの一意性と予測不可能性の完全な喪失**: アプリケーション全体で生成される全てのセッションID、CSRFトークン、APIキー、データベース識別子などが、すべて同一かつ予測可能な "uuuuuuuuuuuuuuuuuuuuu" になります。
2. **認証バイパスとセッションハイジャック**: 攻撃者は、他のユーザーに発行されたトークンを容易に予測できるようになり、セッションハイジャックや認証バイパスが可能になります。
3. **永続的な影響**: 脆弱性は単一のリクエストでトリガーされ、その影響はプロセス全体に及び、プロセスが再起動するまで持続します。これはつまり、悪意のある単一のリクエストが、稼働中のアプリケーションのセキュリティ機能を完全に破壊し続けることを意味します。
4. **低い攻撃コスト**: 特殊な権限や事前条件は不要であり、単一の未認証リクエストで十分です。
PoC (概念実証)
提供されているPoCは以下の通りです。このコードを実行すると、一度大きな`size`を渡した後、全てのIDが "uuuuuuuuuuuuuuuuuuuuu" になることが確認できます。
`import { nanoid } from 'nanoid'`
`// Step 1: 通常の動作`
`console.log(nanoid()) // 例: "V1StGXR8_Z5jdHi6B-myT"`
`// Step 2: オーバーフローをトリガー(例: APIパラメータから)`
`try { nanoid(2147483648) } catch(e) {}`
`// Step 3: 以降の全てのIDが決定論的になる`
`console.log(nanoid()) // "uuuuuuuuuuuuuuuuuuuuu"`
`console.log(nanoid()) // "uuuuuuuuuuuuuuuuuuuuu"`
`console.log(nanoid()) // "uuuuuuuuuuuuuuuuuuuuu"`
`// ... 以降、プロセス全体で永久に続く`
実行コマンド: `node --experimental-vm-modules poc.mjs`
対策:今すぐできること
この脆弱性への対策は、主に以下の2点です。
この脆弱性が修正された`nanoid`のバージョンに可能な限り速やかにアップデートしてください。セキュリティアドバイザリでは特定の修正バージョンが明記されていませんが、最新バージョンへの更新が最も確実な対策です。`npm update nanoid`または`yarn upgrade nanoid`を実行し、`package-lock.json`または`yarn.lock`を更新してください。
アプリケーションでユーザーからの入力を`nanoid`の`size`パラメータに渡している箇所がないか確認し、もしある場合は厳格なバリデーションを導入してください。以下のような対策が考えられます。
* **数値への型変換**: 入力が数値であることを確認し、`parseInt()`などで整数に変換する。
* **範囲チェック**: `size`が妥当な範囲内にあることを確認する。例えば、`1`から`1024`(またはアプリケーションが必要とする最大値)など、現実的な上限と下限を設定し、それ以外の値は拒否するか、デフォルト値にフォールバックさせる。
* **負の値のチェック**: 意図せず負の値が渡されないようにチェックする。
* **デフォルト値の利用**: ユーザーが`size`を制御できない場合は、デフォルトの`nanoid()`(引数なし)を利用するか、アプリケーションで固定の値を設定する。
* **例(擬似コード)**: `const safeSize = Math.min(Math.max(1, parseInt(userInputSize, 10)), 256); if (isNaN(safeSize)) { /* エラーハンドリングまたはデフォルト値 */ } nanoid(safeSize);`
まとめ
`nanoid`の整数オーバーフロー脆弱性 (GHSA-xwg4-73v4-xw9w) は、アプリケーションのセキュリティ根幹を揺るがす重大な問題です。特にユーザー入力をIDの長さに利用しているアプリケーションでは、速やかなライブラリアップデートと入力検証の実装が不可欠です。
フロントエンドエンジニアの皆さんも、バックエンドとの連携部分や、Node.jsでIDを生成しているケースなど、`nanoid`の利用箇所を改めて確認し、サービスの安全性を確保するための対応を怠らないようにしましょう。この機会に、全てのユーザー入力に対するバリデーションの重要性を再認識し、より堅牢なアプリケーション開発を心がけていきましょう。