CVE・GHSA・OSVは何が違うのか 依存の脆弱性照合を支える3つの仕組み

「このライブラリに既知の脆弱性があります」という警告は、どこから来ているのでしょうか。npm auditもGitHubのDependabotも、裏では脆弱性データベースを引いています。ただ、そのデータベースは一枚岩ではありません。CVE・GHSA・OSVという三つの名前が出てきて、役割はそれぞれ違います。違いが分かると、警告の読み方も、照合を自前で組むときの設計も見通しがよくなります。

CVE:脆弱性につける共通の背番号

CVE(Common Vulnerabilities and Exposures)は、脆弱性ひとつひとつにCVE-2021-44228のような一意の番号を振る仕組みです。1999年に始まり、現在はMITREが運営しています。

解決したのは名前の問題でした。同じ脆弱性を、ベンダーごとに違う名前で呼んでいたためです。番号が共通なら、報告書とパッチと検知ツールが同じものを指していると確認できます。2021年12月、Log4ShellがCVE-2021-44228という一つの番号で世界中に同時に伝わったのは、この仕組みがあったからです。

番号を振るのはMITREだけではありません。CNA(CVE Numbering Authority)として認定された組織(主要ベンダーやオープンソース財団、GitHubなど)が、自分の管轄範囲について番号を発行できます。採番を分散させることで、報告から公開までの時間を短くしています。

CVEだけでは足りませんでした

ただ、CVEはソフトウェア全般を対象にした汎用の仕組みです。パッケージエコシステム(npmやPyPIのような、言語ごとの部品配布の仕組み)との相性は良くありません。理由は三つあります。

影響範囲が散文で書かれています。 「Apache Log4j2 2.0-beta9から2.15.0まで」。人ならこれで通じます。機械はそうはいきません。「いま入っている2.14.1は該当するか」を判定するには、この文を解釈しなければなりません。しかも書き方に決まりがないので、表記の揺れも多くなります。

バージョンの比較規則がエコシステムごとに違います。 npmのsemver、PythonのPEP 440、Goの疑似バージョン、Mavenのバージョン順序。同じ「2.0以上3.0未満」を表すのにも、書き方と比べ方が別々になっています。汎用の形式ひとつで表そうとすると、どこかで無理が出ます。

すべての脆弱性にCVEが振られるわけではありません。 エコシステム内で報告され、パッチが出て、CVE番号を取らずに終わるものは珍しくありません。CVEだけを見ていると、これらが丸ごと視界から消えます。

GHSA:エコシステムに寄せたデータベース

そこで各エコシステムが独自のデータベースを持ちはじめました。よく知られているのはGitHub Advisory DatabaseのGHSAで、GHSA-jfh8-c2jp-5v3qのような形式のIDを持ちます。ほかにPythonのPyPA Advisory Database、RustのRustSec、GoのGo Vulnerability Databaseがあります。

これらは対象パッケージ名とバージョン範囲を、そのエコシステムの規則で機械可読に持ちます。CVEがあるものにはCVE番号を併記し、ないものにも独自IDを振ります。実務で効くのは後者です。CVE未採番の脆弱性も拾えます。

一方で、データベースが分かれたことで別の問題が生まれました。形式もバラバラになったことです。ツールを作る側は、npm用・Python用・Rust用にそれぞれ読み取り処理を書くことになります。同じ脆弱性が複数のデータベースに別IDで載っていて、重複を排除する必要も出てきます。

OSV:形式を揃えて、まとめて引けるようにする

OSV(Open Source Vulnerabilities)は、この形式を統一する試みです。Googleが中心となってOSV Schemaという共通のJSON形式を定め、各エコシステムのデータベースをその形式に変換して osv.devに集約しています。

要点はaffectedという構造にあります。パッケージ名とエコシステムを明示したうえで、バージョン範囲を「導入されたバージョン(introduced)」と「修正されたバージョン(fixed)」の組で表します。散文ではありません。突き合わせがそのまま実装できます。

aliasesフィールドで別IDとの対応も持ちます。GHSAとCVEが同じ脆弱性を指していれば、そこで結びつきます。重複の排除がデータ側で解決されています。

取り下げられた勧告のためのwithdrawnフィールドもあります。誤りだった、あるいは重複だったと後から判明した勧告は、削除ではなく取り下げとして記録されます。ツール側は、一度出した警告を撤回する判断ができます。

実際に照合すると何が起きるか

手元のプロジェクトと照合する手順は、単純化すると三段階になります。

  1. いま何が入っているかを確定する:package-lock.jsonやpnpm-lock.yamlのようなロックファイルを読む
  2. エコシステムとパッケージ名で候補を絞る:同名でもnpmとPyPIは別物なので、エコシステムの区別が要る
  3. バージョン範囲に入るかを判定する:ここでエコシステムごとの比較規則を使う

一段階目が要点です。package.jsonの^1.2.0という記述では、実際に入る版が決まりません。ロックファイルがないプロジェクトは照合できません。 「版が固定されていない」こと自体が、脆弱性の有無以前の問題になります。

もう一つ効いてくるのが、間接依存(トランジティブ依存)の扱いです。直接インストールした覚えのないパッケージが、依存の依存として入っています。実際の警告の多くはここから出ます。ロックファイルは依存ツリー全体を記録しているので、この層まで照合できます。

ツールから見ると、形式の統一は効き方が大きくなります。npm向け・Python向けと別々に書いていた照合の処理が、一本にまとまります。対応するエコシステムを増やすときも、足すのはバージョン比較の規則だけで済みます。

「該当する」と「危ない」は別です

照合で「該当」と出ても、それがそのまま危険を意味するとは限りません。脆弱性のある関数を一度も呼んでいない、という状況は普通にあります。到達可能性(reachability)まで見ないと、実際に影響があるかは分かりません。

CVSSスコアも同じです。あれは脆弱性そのものの深刻度であって、そのアプリケーションでの深刻度ではありません。外部に公開していないバッチ処理と、認証前のエンドポイント。同じスコアでも意味がまるで違います。

「該当するが影響しない」を機械可読に記録するためのVEX(Vulnerability Exploitability eXchange)のような取り組みもあります。ただし判断そのものは人が下します。データベースが答えるのは「入っているか」までで、「危ないか」は文脈の問題になります。

通信せずに照合する

osv.devはAPIを公開しています。ただし照合のたびに問い合わせると、どのパッケージを使っているかが外部に伝わります。依存構成は、それ自体が知られたくない情報であることも多いものです。社内システムの構成が、脆弱性チェックのついでに漏れていきます。

OSVはデータベース全体のダンプも配布しています。手元に落として定期的に更新すれば、照合は完全にローカルで完結します。日次や月次の鮮度で足りる用途なら、この形が扱いやすくなります。オフラインの検査環境でも動きます。

まとめ

CVEは共通の背番号、GHSAなどは各エコシステムの実務データベース、OSVはそれらを共通形式に揃えて集約する層。そういう重なりになっています。どれかが他を置き換えるわけではなく、役割が違います。

そして照合の精度は、データベースの質だけでは決まりません。ロックファイルで版が固定されているかというプロジェクト側の状態。該当した脆弱性が実際に到達可能かという文脈の判断。この二つが、警告を実務で使えるものにするかどうかを分けます。

自社やお取引先のプロジェクトを確かめるなら、まずロックファイルがリポジトリに入っているかを見てください。入っていなければ、どんな検査ツールを導入しても結果は当てになりません。あわせて、脆弱性の照合がどこへ問い合わせているか(外部APIか、手元のデータか)も確認しておくと、依存構成が外に出る経路を把握できます。

まずはお気軽にご相談ください。目的や課題を丁寧にヒアリングし、
ご予算や納期に合わせた最適なご提案をいたします。

まずはお気軽にご相談ください。
目的や課題を丁寧にヒアリングし、
ご予算や納期に合わせた最適な
ご提案をいたします。

無料相談はこちら