diff options
-rw-r--r-- | .travis.yml | 19 | ||||
-rw-r--r-- | Rakefile | 2 | ||||
-rw-r--r-- | lib/bundler/definition.rb | 3 | ||||
-rw-r--r-- | lib/bundler/resolver.rb | 8 | ||||
-rw-r--r-- | lib/bundler/rubygems_gem_installer.rb | 36 | ||||
-rw-r--r-- | lib/bundler/settings.rb | 1 | ||||
-rw-r--r-- | lib/bundler/source/rubygems.rb | 3 | ||||
-rw-r--r-- | spec/commands/lock_spec.rb | 91 | ||||
-rw-r--r-- | spec/install/gems/compact_index_spec.rb | 32 | ||||
-rw-r--r-- | spec/support/artifice/compact_index.rb | 7 | ||||
-rw-r--r-- | spec/support/artifice/compact_index_wrong_gem_checksum.rb | 19 | ||||
-rw-r--r-- | spec/support/matchers.rb | 6 |
12 files changed, 196 insertions, 31 deletions
diff --git a/.travis.yml b/.travis.yml index 223cce066a..88ae318a94 100644 --- a/.travis.yml +++ b/.travis.yml @@ -109,23 +109,4 @@ matrix: allow_failures: - rvm: 1.8.7 - env: RGV=v2.1.11 - - rvm: 1.8.7 - env: RGV=v2.2.5 - - rvm: 1.8.7 - env: RGV=v2.0.14 - - rvm: 1.8.7 - env: RGV=v1.8.29 - - rvm: 1.8.7 - env: RGV=v1.7.2 - - rvm: 1.8.7 - env: RGV=v1.6.2 - - rvm: 1.8.7 - env: RGV=v1.5.3 - - rvm: 1.8.7 - env: RGV=v1.4.2 - - rvm: 1.8.7 - env: RGV=v1.3.7 - - rvm: 1.8.7 - env: RGV=v1.3.6 - rvm: ruby-head @@ -127,7 +127,7 @@ begin rubyopt = ENV["RUBYOPT"] # When editing this list, also edit .travis.yml! branches = %w(master) - releases = %w(v1.3.6 v1.3.7 v1.4.2 v1.5.3 v1.6.2 v1.7.2 v1.8.29 v2.0.14 v2.1.11 v2.2.5 v2.4.8 v2.6.6) + releases = %w(v1.3.6 v1.3.7 v1.4.2 v1.5.3 v1.6.2 v1.7.2 v1.8.29 v2.0.14 v2.1.11 v2.2.5 v2.4.8 v2.5.2 v2.6.6) (branches + releases).each do |rg| desc "Run specs with Rubygems #{rg}" RSpec::Core::RakeTask.new(rg) do |t| diff --git a/lib/bundler/definition.rb b/lib/bundler/definition.rb index 2d328e0de1..e1826746ff 100644 --- a/lib/bundler/definition.rb +++ b/lib/bundler/definition.rb @@ -816,7 +816,8 @@ module Bundler def additional_base_requirements_for_resolve return [] unless @locked_gems && Bundler.settings[:only_update_to_newer_versions] @locked_gems.specs.reduce({}) do |requirements, locked_spec| - requirements[locked_spec.name] = Gem::Dependency.new(locked_spec.name, ">= #{locked_spec.version}") + dep = Gem::Dependency.new(locked_spec.name, ">= #{locked_spec.version}") + requirements[locked_spec.name] = DepProxy.new(dep, locked_spec.platform) requirements end.values end diff --git a/lib/bundler/resolver.rb b/lib/bundler/resolver.rb index 10d5404028..e1d993831f 100644 --- a/lib/bundler/resolver.rb +++ b/lib/bundler/resolver.rb @@ -189,7 +189,10 @@ module Bundler @resolver = Molinillo::Resolver.new(self, self) @search_for = {} @base_dg = Molinillo::DependencyGraph.new - @base.each {|ls| @base_dg.add_vertex(ls.name, Dependency.new(ls.name, ls.version), true) } + @base.each do |ls| + dep = Dependency.new(ls.name, ls.version) + @base_dg.add_vertex(ls.name, DepProxy.new(dep, ls.platform), true) + end additional_base_requirements.each {|d| @base_dg.add_vertex(d.name, d) } @ruby_version = ruby_version @gem_version_promoter = gem_version_promoter @@ -303,7 +306,8 @@ module Bundler end def requirement_satisfied_by?(requirement, activated, spec) - requirement.matches_spec?(spec) || spec.source.is_a?(Source::Gemspec) + return false unless requirement.matches_spec?(spec) || spec.source.is_a?(Source::Gemspec) + spec.activate_platform!(requirement.__platform) || spec.for?(requirement.__platform, nil) end def sort_dependencies(dependencies, activated, conflicts) diff --git a/lib/bundler/rubygems_gem_installer.rb b/lib/bundler/rubygems_gem_installer.rb index e18f46268b..0aa9fd91d6 100644 --- a/lib/bundler/rubygems_gem_installer.rb +++ b/lib/bundler/rubygems_gem_installer.rb @@ -12,5 +12,41 @@ module Bundler def check_executable_overwrite(filename) # Bundler needs to install gems regardless of binstub overwriting end + + def pre_install_checks + super && validate_bundler_checksum(options[:bundler_expected_checksum]) + end + + private + + def validate_bundler_checksum(checksum) + return true if Bundler.settings[:disable_checksum_validation] + return true unless checksum + return true unless source = @package.instance_variable_get(:@gem) + return true unless source.respond_to?(:with_read_io) + digest = source.with_read_io do |io| + digest = Digest::SHA256.new + digest << io.read(16_384) until io.eof? + io.rewind + digest.send(checksum_type(checksum)) + end + unless digest == checksum + raise SecurityError, + "The checksum for the downloaded `#{spec.full_name}.gem` did not match " \ + "the checksum given by the API. This means that the contents of the " \ + "gem appear to be different from what was uploaded, and could be an indicator of a security issue.\n" \ + "(The expected SHA256 checksum was #{checksum.inspect}, but the checksum for the downloaded gem was #{digest.inspect}.)\n" \ + "Bundler cannot continue installing #{spec.name} (#{spec.version})." + end + true + end + + def checksum_type(checksum) + case checksum.length + when 64 then :hexdigest! + when 44 then :base64digest! + else raise InstallError, "The given checksum for #{spec.full_name} (#{checksum.inspect}) is not a valid SHA256 hexdigest nor base64digest" + end + end end end diff --git a/lib/bundler/settings.rb b/lib/bundler/settings.rb index 365d20adda..c5fd46d440 100644 --- a/lib/bundler/settings.rb +++ b/lib/bundler/settings.rb @@ -6,6 +6,7 @@ module Bundler BOOL_KEYS = %w( allow_offline_install cache_all + disable_checksum_validation disable_exec_load disable_local_branch_check disable_shared_gems diff --git a/lib/bundler/source/rubygems.rb b/lib/bundler/source/rubygems.rb index aedad7086d..89f7673eb8 100644 --- a/lib/bundler/source/rubygems.rb +++ b/lib/bundler/source/rubygems.rb @@ -140,7 +140,8 @@ module Bundler :bin_dir => bin_path.to_s, :ignore_dependencies => true, :wrappers => true, - :env_shebang => true + :env_shebang => true, + :bundler_expected_checksum => spec.respond_to?(:checksum) && spec.checksum ).install end spec.full_gem_path = installed_spec.full_gem_path diff --git a/spec/commands/lock_spec.rb b/spec/commands/lock_spec.rb index 1e189a9659..fc605dc5a6 100644 --- a/spec/commands/lock_spec.rb +++ b/spec/commands/lock_spec.rb @@ -132,4 +132,95 @@ describe "bundle lock" do bundle "lock --remove-platform #{local}" expect(out).to include("Removing all platforms from the bundle is not allowed") end + + # from https://github.com/bundler/bundler/issues/4896 + it "properly adds platforms when platform requirements come from different dependencies" do + build_repo4 do + build_gem "ffi", "1.9.14" + build_gem "ffi", "1.9.14" do |s| + s.platform = mingw + end + + build_gem "gssapi", "0.1" + build_gem "gssapi", "0.2" + build_gem "gssapi", "0.3" + build_gem "gssapi", "1.2.0" do |s| + s.add_dependency "ffi", ">= 1.0.1" + end + + build_gem "mixlib-shellout", "2.2.6" + build_gem "mixlib-shellout", "2.2.6" do |s| + s.platform = "universal-mingw32" + s.add_dependency "win32-process", "~> 0.8.2" + end + + # we need all these versions to get the sorting the same as it would be + # pulling from rubygems.org + %w(0.8.3 0.8.2 0.8.1 0.8.0).each do |v| + build_gem "win32-process", v do |s| + s.add_dependency "ffi", ">= 1.0.0" + end + end + end + + gemfile <<-G + source "file:#{gem_repo4}" + + gem "mixlib-shellout" + gem "gssapi" + G + + simulate_platform(mingw) { bundle! :lock } + + expect(the_bundle.lockfile).to read_as(strip_whitespace(<<-G)) + GEM + remote: file:#{gem_repo4}/ + specs: + ffi (1.9.14-x86-mingw32) + gssapi (1.2.0) + ffi (>= 1.0.1) + mixlib-shellout (2.2.6-universal-mingw32) + win32-process (~> 0.8.2) + win32-process (0.8.3) + ffi (>= 1.0.0) + + PLATFORMS + x86-mingw32 + + DEPENDENCIES + gssapi + mixlib-shellout + + BUNDLED WITH + #{Bundler::VERSION} + G + + simulate_platform(rb) { bundle! :lock } + + expect(the_bundle.lockfile).to read_as(strip_whitespace(<<-G)) + GEM + remote: file:#{gem_repo4}/ + specs: + ffi (1.9.14) + ffi (1.9.14-x86-mingw32) + gssapi (1.2.0) + ffi (>= 1.0.1) + mixlib-shellout (2.2.6) + mixlib-shellout (2.2.6-universal-mingw32) + win32-process (~> 0.8.2) + win32-process (0.8.3) + ffi (>= 1.0.0) + + PLATFORMS + ruby + x86-mingw32 + + DEPENDENCIES + gssapi + mixlib-shellout + + BUNDLED WITH + #{Bundler::VERSION} + G + end end diff --git a/spec/install/gems/compact_index_spec.rb b/spec/install/gems/compact_index_spec.rb index 0edd1d20e7..a800a6ad7b 100644 --- a/spec/install/gems/compact_index_spec.rb +++ b/spec/install/gems/compact_index_spec.rb @@ -695,4 +695,36 @@ The checksum of /versions does not match the checksum provided by the server! So expect(File.read(versions)).to start_with("created_at") expect(the_bundle).to include_gems "rack 1.0.0" end + + describe "checksum validation", :rubygems => ">= 2.3.0" do + it "raises when the checksum does not match" do + install_gemfile <<-G, :artifice => "compact_index_wrong_gem_checksum" + source "#{source_uri}" + gem "rack" + G + expect(exitstatus).to eq(19) if exitstatus + expect(out). + to include("The checksum for the downloaded `rack-1.0.0.gem` did not match the checksum given by the API."). + and include("This means that the contents of the gem appear to be different from what was uploaded, and could be an indicator of a security issue."). + and match(/\(The expected SHA256 checksum was "#{"ab" * 22}", but the checksum for the downloaded gem was ".+?"\.\)/). + and include("Bundler cannot continue installing rack (1.0.0).") + end + + it "raises when the checksum is the wrong length" do + install_gemfile <<-G, :artifice => "compact_index_wrong_gem_checksum", :env => { "BUNDLER_SPEC_RACK_CHECKSUM" => "checksum!" } + source "#{source_uri}" + gem "rack" + G + expect(exitstatus).to eq(5) if exitstatus + expect(out).to include("The given checksum for rack-1.0.0 (\"checksum!\") is not a valid SHA256 hexdigest nor base64digest") + end + + it "does not raise when disable_checksum_validation is set" do + bundle! "config disable_checksum_validation true" + install_gemfile! <<-G, :artifice => "compact_index_wrong_gem_checksum" + source "#{source_uri}" + gem "rack" + G + end + end end diff --git a/spec/support/artifice/compact_index.rb b/spec/support/artifice/compact_index.rb index 233c192a67..f3ff2db4fa 100644 --- a/spec/support/artifice/compact_index.rb +++ b/spec/support/artifice/compact_index.rb @@ -78,7 +78,12 @@ class CompactIndexAPI < Endpoint reqs = d.requirement.requirements.map {|r| r.join(" ") }.join(", ") CompactIndex::Dependency.new(d.name, reqs) end - CompactIndex::GemVersion.new(spec.version.version, spec.platform.to_s, nil, nil, + checksum = begin + Digest::SHA256.file("#{GEM_REPO}/gems/#{spec.original_name}.gem").base64digest + rescue + nil + end + CompactIndex::GemVersion.new(spec.version.version, spec.platform.to_s, checksum, nil, deps, spec.required_ruby_version, spec.required_rubygems_version) end CompactIndex::Gem.new(name, gem_versions) diff --git a/spec/support/artifice/compact_index_wrong_gem_checksum.rb b/spec/support/artifice/compact_index_wrong_gem_checksum.rb new file mode 100644 index 0000000000..3a12a59ae7 --- /dev/null +++ b/spec/support/artifice/compact_index_wrong_gem_checksum.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true +require File.expand_path("../compact_index", __FILE__) + +Artifice.deactivate + +class CompactIndexWrongGemChecksum < CompactIndexAPI + get "/info/:name" do + etag_response do + name = params[:name] + gem = gems.find {|g| g.name == name } + checksum = ENV.fetch("BUNDLER_SPEC_#{name.upcase}_CHECKSUM") { "ab" * 22 } + versions = gem ? gem.versions : [] + versions.each {|v| v.checksum = checksum } + CompactIndex.info(versions) + end + end +end + +Artifice.activate_with(CompactIndexWrongGemChecksum) diff --git a/spec/support/matchers.rb b/spec/support/matchers.rb index 9476f18984..9248360639 100644 --- a/spec/support/matchers.rb +++ b/spec/support/matchers.rb @@ -110,15 +110,9 @@ module Spec define_compound_matcher :read_as, [exist] do |file_contents| diffable - attr_reader :strip_whitespace - - chain :stripping_whitespace do - @strip_whitespace = true - end match do |actual| @actual = Bundler.read_file(actual) - file_contents = strip_whitespace(file_contents) if strip_whitespace values_match?(file_contents, @actual) end end |