こんにちは、金子です。 普段はRailsを書いたりしています。
今回は2016/4/6に発表された、RubyGems.orgの脆弱性についてまとめました。
脆弱性について
RubyGems.org gem replacement vulnerability and mitigation をざっと読んでみると、
- 特定の状況で、RubyGems.orgにupdateされているファイルの内容が不正に書き換えられる可能性があった
- 特定の状況とは、2014-6-11から2016-4-2までの間に登録されたgemのうち、'blank-blank'のように名前に
'-'
(dash)が入っているもの - ただし2015-2-8以降に登録されたgemはRubyGems.orgがsha256 checksumを計算しており、それと実際のファイルの突合をして、書き換えられていないことを確認ずみ
つまり、2014-6-11から2015-2-7の間に登録されたgemが、今回の脆弱性の範囲にあたるということです。
自分が使っているgemのうち、影響のありそうなものを調べる
自分の使っているgemの一覧を取得する
例えばRailsでアプリケーションの開発を行っており、bundlerでgemの管理をしているようなプロジェクトでは、Gemfile.lock
に現在使用しているgemの名前とversionが記録されています。
例えば以下のようなコードで、名前に'-'
が入っているgemの一覧が取得できます。
lockfile = Bundler::LockfileParser.new(Bundler.read_file("path_to_Gemfile.lock")) gems = lockfile.specs.select {|s| s.name =~ /-/}.map {|s| [s.name, s.version.to_s]}
gemの名前とversionから登録日時を調べる
RubyGems.org Data Dumps からRubyGems.orgのPostgreSQLのデータが入手できるので、こちら を参考に、自分の環境にDBを作成します。
直接DBを操作してもいいのですが、今回はRailsでラップしてみます。
rails _4.2.6_ new -d postgresql rubygems
などで新規にプロジェクトを作成し、
app/models/rubygem.rb
とapp/models/version.rb
を作成します。
# app/models/rubygem.rb class Rubygem < ActiveRecord::Base has_many :versions end # app/models/version.rb class Version < ActiveRecord::Base belongs_to :rubygem end
またデフォルトではrubygems
という名前のdatabaseにデータが入るので、config/database.yml
も調整します。
# config/database.yml development: <<: *default database: rubygems
準備ができたら以下のようなスクリプトを実行します。gem list
の内容をもとにチェックしたいというケースもあるかと思いますので、GemVersion
というクラスも用意しました。
class DangerousGemValidator def initialize refresh end def invalid_gems(gems) _invalid_gems(gems).map do |name, number, rubygem, version| [name, number, version.created_at.strftime('%Y-%m-%d %H:%M:%S')] end end def excepted_records { rubygems_has_no_records: @rubygems_has_no_records.sort.uniq, rubygems_has_multiple_records: @rubygems_has_multiple_records.sort.uniq, versions_has_not_just_one_records: @versions_has_not_just_one_records.sort.uniq } end private def _invalid_gems(gems) return @invalid_gems if @invalid_gems @invalid_gems = gems.each_with_object([]) do |(name, number), array| rubygems = Rubygem.where(name: name) if rubygems.count == 0 @rubygems_has_no_records << "#{name} has no record" next end if rubygems.count != 1 @rubygems_has_multiple_records << "#{name} has #{rubygems.count} record(s)" next end rubygem = rubygems.first versions = rubygem.versions.where(number: number) case when versions.count == 1 version = versions.first when versions.where(platform: 'ruby').exists? version = versions.find_by(platform: 'ruby') else @versions_has_not_just_one_records << "#{name} has #{versions.count} versions record(s) (number: #{number}, rubygem.id: #{rubygem.id})" next end if (Time.utc(2014, 6, 11) <= version.created_at) && (version.created_at <= Time.utc(2015, 2, 18)) array << [name, number, rubygem, version] end end end def refresh @invalid_gems = nil @rubygems_has_no_records = [] @rubygems_has_multiple_records = [] @versions_has_not_just_one_records = [] end end class GemVersion def initialize(file_path) @file_path = file_path end def gems File.read(@file_path).each_line.each_with_object([]) do |line, array| line =~ /([\w-]*) \((.*)\)/ gem_name = $1 versions = $2.split(', ') next if gem_name !~ /-/ array.concat versions.map {|v| [gem_name, v]} end end end gem_version = GemVersion.new("path_to_gem_list_text") validator = DangerousGemValidator.new # gem listをもとに検索する validator.invalid_gems(gem_version.gems) # or Gemfile.lockをもとに検索する lockfile = Bundler::LockfileParser.new(Bundler.read_file("path_to_Gemfile.lock")) gems = lockfile.specs.select {|s| s.name =~ /-/}.map {|s| [s.name, s.version.to_s]} validator.invalid_gems(gems)
ためしに、今つくったRailsアプリ(rubygemsアプリのこと、Rails 4.2.6の標準設定)のGemfile.lock
に対して実行してみます。
lockfile = Bundler::LockfileParser.new(Bundler.read_file(File.join(Rails.root, '/Gemfile.lock'))) gems = lockfile.specs.select {|s| s.name =~ /-/}.map {|s| [s.name, s.version.to_s]} validator.invalid_gems(gems) => [["rack-test", "0.6.3", "2015-01-09 17:58:13"], ["rails-deprecated_sanitizer", "1.0.3", "2014-09-25 16:33:44"]]
おや、rails-deprecated_sanitizer
がひっかかりました。rubygems.org をみてみると、確かに。。。
gemの内容を確認する
RubyGems.org gem replacement vulnerability and mitigation の"WHAT SHOULD I DO?"をもとに、rails-deprecated_sanitizer
の1.0.3
を確認してみましょう。
まずはgemをインストールして、unpackします。
$ gem install rails-deprecated_sanitizer -v 1.0.3 Successfully installed rails-deprecated_sanitizer-1.0.3 1 gem installed $ gem unpack rails-deprecated_sanitizer -v 1.0.3 Unpacked gem: '/.../rails-deprecated_sanitizer-1.0.3'
次に、コードをダウンロードし、リリース時のコミットをチェックアウトします。
$ git clone git@github.com:rails/rails-deprecated_sanitizer.git $ cd rails-deprecated_sanitizer $ git checkout v1.0.3
diff
を取ります。
$ diff -r rails-deprecated_sanitizer-1.0.3/ rails-deprecated_sanitizer Only in rails-deprecated_sanitizer: .git Only in rails-deprecated_sanitizer: .gitignore Only in rails-deprecated_sanitizer: .travis.yml Only in rails-deprecated_sanitizer: Gemfile Only in rails-deprecated_sanitizer: LICENSE.txt Only in rails-deprecated_sanitizer: Rakefile Only in rails-deprecated_sanitizer: rails-deprecated_sanitizer.gemspec
rails-deprecated_sanitizer
にしかないファイルが複数ありますが、これはrails-deprecated_sanitizer.gemspec
の設定で、パッケージに含めていないものなので、問題なさそうです。
まとめ
マネーフォワードの本番環境で使用している全gemを対象に上記の絞り込みを行い、差分の確認をし、全gemに問題がないことを確かめました。
最後に
マネーフォワードでは、RubyやRailsをいじり倒すのが大好きというエンジニアを募集しています。 ご応募お待ちしています。
【採用サイト】 ■マネーフォワード採用サイト ■Wantedly | マネーフォワード
【プロダクト一覧】 ■家計簿アプリ・クラウド家計簿ソフト『マネーフォワード』 ■家計簿アプリ・クラウド家計簿ソフト『マネーフォワード』 iPhone,iPad ■家計簿アプリ・クラウド家計簿ソフト『マネーフォワード』 Android ■クラウド型会計ソフト『MFクラウド会計』 ■クラウド型請求書管理ソフト『MFクラウド請求書』 ■クラウド型給与計算ソフト『MFクラウド給与』 ■経費精算システム『MFクラウド経費』 ■消込ソフト・システム『MFクラウド消込』 ■マイナンバー対応『MFクラウドマイナンバー』 ■創業支援トータルサービス『MFクラウド創業支援サービス』 ■お金に関する正しい知識やお得な情報を発信するウェブメディア『マネトク!』