Composerを活用した脆弱性ハンドリングについて

PHPカンファレンス福岡2016にて、「Composerを活用した脆弱性ハンドリング」というタイトルでLTを行いました。

スライドと制作物のご紹介、今後の展開などについて個人的な覚書として書きます。

TL;DR

  • Composer(というかPackagist)に掲載のパッケージが、脆弱性を持っているかどうかの情報を返すAPIを作りました。
  • そのAPIにアクセスする、専用のComposerプラグインを作りました。(tisayama/composer-vuln-handler)
  • Packagistのパッケージ情報と脆弱性情報のアグリゲーションには大いに問題があり、自動判別などがまったくうまくいってないので、うまく紐付いていない情報などがあれば教えてください。

スライド

内容

用語

CVE®(Common Vulnerabilities and Exposures)…「共通脆弱性指定子」米国の非営利団体MITRE社が採番している、脆弱性の識別番号

NVD(National Vulnerability Database)…米国の国立標準技術研究所が運営している脆弱性情報データベース

CPE(Common Platform Enumeration)…「共通プラットフォーム一覧」情報資産を構成する要素の名称を正規化するルール。米国の非営利団体MITRE社が策定に関与。

背景

会社で、使用しているPHPフレームワークのバージョンなどは管理しているものの、何がどう危ないと言った話がRSSフィードの目grepからしか得られていないという事情があり、もっとお手軽に脆弱性情報を得る方法を模索していました。

作りたかったもの

脆弱性情報を簡単に得られる仕組み、特に、影響するパッケージを導入しようとすると画面表示で教えてくれるような親切な仕組みがあると嬉しいのではないかと考えました。

これをPHPに落とし込むと、Composerでインストールしようとすると画面表示で教えてくれるのが理想かと思います。

先行事例

Symfonyのスポンサーで、かの有名なSensioLabsさんがSecurity Advisories Checkerというものを運営しておりました。これはcomposer.lockファイルをアップロードするなどすれば脆弱性のあるバージョンのパッケージを使用していないか検査してくれるツール及びサービスです。

書いてある説明書きを読み込むとわかるのですが、これはGitHubのContributeを通して人間が登録したアドバイザリ情報を提供するものになります。つまり人間が登録しようと思わなかった脆弱性情報は登録されません。

例えば、現時点ではCakePHPの今年の脆弱性(CVE-2015-8379)が登録されていなかったり、EC-CUBEの項目自体がなかったりします。

一方で人間が介在する強みもあり、CVEが立っていない、つまり脆弱性データベースに登録されていないものも手作業で登録されています。例えば昨年夏の、CakePHPの認証がかかったprefix routing配下に直接アクセスを許してしまう脆弱性はきちんと登録されています。

いずれにせよ、NVDに登録済みのCVEを取りこぼしているのはあまり良い話ではないので、ある程度は自動化したソリューションが必要ではないかと思います。

今回の製作物

今回、ローカルのComposerリポジトリの情報をもとに、APIに脆弱性情報がないか問い合わせるComposerプラグインおよび、APIエンドポイントとComposerとNVDのアグリゲーションプログラムを開発しました。

ComposerとNVDのアグリゲーション手法

ComposerのリポジトリのPackagistでは、パッケージの名称の形式が決められており、例えばCakePHPだったら”cakephp/cakephp” 、Symfonyだったら”symfony/symfony”などとパッケージ名称の正規化ルールがあります。

いっぽう、NVDでも同様に、影響するソフトウェアなどの名称、バージョンの正規化表記ルールがあります。それがCPE(Common Platform Enumeration)です。たとえば、CakePHPであれば、以下のように表されます。

cpe:/a:cakephp:cakephp:2.0.4

先頭のcpe:/に続く、aがソフトウェアの種別、次の:に囲まれた部分がベンダー名、次がソフトウェア名、最後がバージョンです。場合によっては後ろにアップデート名などが続きます。(CPE 2.2の場合)

表記ゆれなどの問題

ここまでの話ですと、先ほどのComposerでのパッケージ情報の表記方法からこのCPEへの変換は容易に思えます。しかし、話はそんなに簡単ではありません。実はCakePHPにはもう一つのCPEがあります。

cpe:/a:cakefoundation:cakephp:2.2.3

ベンダー名がcakefoundationになりました。また、Symfonyは以下のようになります。

cpe:/a:sensiolabs:symfony:2.3.12

Composerでの表記と違って、ベンダー部分がsensiolabsになっています。これらの表記ゆれ、情報不足などによりComposerでの表記とCPEとの間での相互変換はなかなか厳しいのです。

マッチング手法のひとまずの解

開発当初、CPE表記一覧であるCPE Dictionaryからの類推により、容易にComposer表記との間で一対一の相互変換が可能になると仮定し、CPE DictionaryをSELECTしまくるプログラムを書いていたので、上記の問題ですぐに行き詰まりました。

方法を変え、Composer側が持っているhomepageやリポジトリのURLと、CPE Dictionaryのアイテムの参考URLを突き合わせて集計を行う試みをやってみました。しかし、少なくともhomepageに関してはまともに入力されていないことが判明し、頓挫しました。(ある程度は頑張れるのですが、ゴミデータが多すぎます)

準備時間は限られていたので、最終的にどうしようもなくなり。ComposerとCPEとの結び付けについてはある程度自動で行うことに決めました。

今回は時間がなく妥協しましたが、今後はComposerバージョンに対して、生成ルールに従って自動で生成したCPEを紐付けて利用したいとも考えています。

脆弱性情報の入手

脆弱性情報はNVDのこのページから、圧縮されたXMLの形式で入手しています。展開した上でMySQLに取り込み、ごりごり処理しています。

Composerプラグイン

脇道にそれた話になりますが、今回製作した、APIクライアント側はComposerプラグインとして動作します。Composerプラグインについてはあまりまとまった情報がなく、若干苦戦したのでまとめておきます。

だいたい以下のような動作をします。

  • プラグインのcomposer.jsonの[‘extra’][‘class’]で指定されたPluginInterfaceを実装したClassのactivate()が呼ばれる
  • 同クラスのgetSubscribedEvents()という静的メソッドでイベントを受けるメソッドを指定する
    • post-autoload-dumpなどのイベント名を指定する
  • イベントを受けるメソッドにはイベントオブジェクトが渡ってくるので、なにか処理する

また、基本的にはプラグインなので、他のcomposer.jsonでrequireされない限りイベントは来ません。開発中に試しにイベントを発火させてみたい場合は、他のcomposer.jsonのほうでrepositoriesにtype:pathと指定したものを追加し、そこからプラグインを追加すると簡単に検証できます。

今後の課題

今回、マッチングの部分で妥協して、データ量に問題があるままリリースしてしまったので、そこは早めに修正したいです。

また、現状データ処理に時間がかかっているので、もっと処理を見直していきたいです。