aboutsummaryrefslogtreecommitdiffstats
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/bundler/checksum.rb246
-rw-r--r--lib/bundler/definition.rb4
-rw-r--r--lib/bundler/endpoint_specification.rb13
-rw-r--r--lib/bundler/errors.rb43
-rw-r--r--lib/bundler/fetcher.rb6
-rw-r--r--lib/bundler/gem_helpers.rb18
-rw-r--r--lib/bundler/lazy_specification.rb18
-rw-r--r--lib/bundler/lockfile_generator.rb11
-rw-r--r--lib/bundler/lockfile_parser.rb20
-rw-r--r--lib/bundler/rubygems_ext.rb21
-rw-r--r--lib/bundler/rubygems_gem_installer.rb25
-rw-r--r--lib/bundler/source/rubygems.rb3
12 files changed, 264 insertions, 164 deletions
diff --git a/lib/bundler/checksum.rb b/lib/bundler/checksum.rb
index fe8e73e727..7539522908 100644
--- a/lib/bundler/checksum.rb
+++ b/lib/bundler/checksum.rb
@@ -2,37 +2,38 @@
module Bundler
class Checksum
+ DEFAULT_ALGORITHM = "sha256"
+ private_constant :DEFAULT_ALGORITHM
DEFAULT_BLOCK_SIZE = 16_384
private_constant :DEFAULT_BLOCK_SIZE
class << self
- def from_gem_source(source, digest_algorithms: %w[sha256])
- raise ArgumentError, "not a valid gem source: #{source}" unless source.respond_to?(:with_read_io)
-
- source.with_read_io do |io|
- checksums = from_io(io, "#{source.path || source.inspect} gem source hexdigest", :digest_algorithms => digest_algorithms)
- io.rewind
- return checksums
- end
+ def from_gem(io, pathname, algo = DEFAULT_ALGORITHM)
+ digest = Bundler::SharedHelpers.digest(algo.upcase).new
+ buf = String.new(:capacity => DEFAULT_BLOCK_SIZE)
+ digest << io.readpartial(DEFAULT_BLOCK_SIZE, buf) until io.eof?
+ Checksum.new(algo, digest.hexdigest!, Source.new(:gem, pathname))
end
- def from_io(io, source, digest_algorithms: %w[sha256])
- digests = digest_algorithms.to_h do |algo|
- [algo.to_s, Bundler::SharedHelpers.digest(algo.upcase).new]
- end
+ def from_api(digest, source_uri)
+ # transform the bytes from base64 to hex, switch to unpack1 when we drop older rubies
+ hexdigest = digest.length == 44 ? digest.unpack("m0").first.unpack("H*").first : digest
- until io.eof?
- ret = io.read DEFAULT_BLOCK_SIZE
- digests.each_value {|digest| digest << ret }
+ if hexdigest.length != 64
+ raise ArgumentError, "#{digest.inspect} is not a valid SHA256 hexdigest nor base64digest"
end
- digests.map do |algo, digest|
- Checksum.new(algo, digest.hexdigest!, source)
- end
+ Checksum.new(DEFAULT_ALGORITHM, hexdigest, Source.new(:api, source_uri))
+ end
+
+ def from_lock(lock_checksum, lockfile_location)
+ algo, digest = lock_checksum.strip.split("-", 2)
+ Checksum.new(algo, digest, Source.new(:lock, lockfile_location))
end
end
attr_reader :algo, :digest, :sources
+
def initialize(algo, digest, source)
@algo = algo
@digest = digest
@@ -62,18 +63,79 @@ module Bundler
end
def merge!(other)
- raise ArgumentError, "cannot merge checksums of different algorithms" unless algo == other.algo
+ return nil unless match?(other)
+ @sources.concat(other.sources).uniq!
+ self
+ end
- unless digest == other.digest
- raise SecurityError, <<~MESSAGE
- #{other}
- #{to_lock} from:
- * #{sources.join("\n* ")}
- MESSAGE
+ def formatted_sources
+ sources.join("\n and ").concat("\n")
+ end
+
+ def removable?
+ sources.all?(&:removable?)
+ end
+
+ def removal_instructions
+ msg = +""
+ i = 1
+ sources.each do |source|
+ msg << " #{i}. #{source.removal}\n"
+ i += 1
end
+ msg << " #{i}. run `bundle install`\n"
+ end
- @sources.concat(other.sources).uniq!
- self
+ def inspect
+ abbr = "#{algo}-#{digest[0, 8]}"
+ from = "from #{sources.join(" and ")}"
+ "#<#{self.class}:#{object_id} #{abbr} #{from}>"
+ end
+
+ class Source
+ attr_reader :type, :location
+
+ def initialize(type, location)
+ @type = type
+ @location = location
+ end
+
+ def removable?
+ type == :lock || type == :gem
+ end
+
+ def ==(other)
+ other.is_a?(self.class) && other.type == type && other.location == location
+ end
+
+ # phrased so that the usual string format is grammatically correct
+ # rake (10.3.2) sha256-abc123 from #{to_s}
+ def to_s
+ case type
+ when :lock
+ "the lockfile CHECKSUMS at #{location}"
+ when :gem
+ "the gem at #{location}"
+ when :api
+ "the API at #{location}"
+ else
+ "#{location} (#{type})"
+ end
+ end
+
+ # A full sentence describing how to remove the checksum
+ def removal
+ case type
+ when :lock
+ "remove the matching checksum in #{location}"
+ when :gem
+ "remove the gem at #{location}"
+ when :api
+ "checksums from #{location} cannot be locally modified, you may need to update your sources"
+ else
+ "remove #{location} (#{type})"
+ end
+ end
end
class Store
@@ -86,89 +148,81 @@ module Bundler
def initialize_copy(other)
@store = {}
- other.store.each do |full_name, checksums|
- store[full_name] = checksums.dup
+ other.store.each do |name_tuple, checksums|
+ store[name_tuple] = checksums.dup
end
end
- def checksums(full_name)
- store[full_name]
+ def inspect
+ "#<#{self.class}:#{object_id} size=#{store.size}>"
end
- def register_gem_package(spec, source)
- new_checksums = Checksum.from_gem_source(source)
- new_checksums.each do |checksum|
- register spec.full_name, checksum
- end
- rescue SecurityError
- expected = checksums(spec.full_name)
- gem_lock_name = GemHelpers.lock_name(spec.name, spec.version, spec.platform)
- raise SecurityError, <<~MESSAGE
- Bundler cannot continue installing #{gem_lock_name}.
- The checksum for the downloaded `#{spec.full_name}.gem` does not match \
- the known checksum for the gem.
- This means the contents of the downloaded \
- gem is different from what was uploaded to the server \
- or first used by your teammates, and could be a potential security issue.
-
- To resolve this issue:
- 1. delete the downloaded gem located at: `#{source.path}`
- 2. run `bundle install`
-
- If you are sure that the new checksum is correct, you can \
- remove the `#{gem_lock_name}` entry under the lockfile `CHECKSUMS` \
- section and rerun `bundle install`.
-
- If you wish to continue installing the downloaded gem, and are certain it does not pose a \
- security issue despite the mismatching checksum, do the following:
- 1. run `bundle config set --local disable_checksum_validation true` to turn off checksum verification
- 2. run `bundle install`
-
- #{expected.map do |checksum|
- next unless actual = new_checksums.find {|c| c.algo == checksum.algo }
- next if actual.digest == checksum.digest
-
- "(More info: The expected #{checksum.algo.upcase} checksum was #{checksum.digest.inspect}, but the " \
- "checksum for the downloaded gem was #{actual.digest.inspect}. The expected checksum came from: #{checksum.sources.join(", ")})"
- end.compact.join("\n")}
- MESSAGE
- end
-
- def register(full_name, checksum)
+ def fetch(spec, algo = DEFAULT_ALGORITHM)
+ store[spec.name_tuple]&.fetch(algo, nil)
+ end
+
+ # Replace when the new checksum is from the same source.
+ # The primary purpose of this registering checksums from gems where there are
+ # duplicates of the same gem (according to full_name) in the index.
+ # In particular, this is when 2 gems have two similar platforms, e.g.
+ # "darwin20" and "darwin-20", both of which resolve to darwin-20.
+ # In the Index, the later gem replaces the former, so we do that here.
+ #
+ # However, if the new checksum is from a different source, we register like normal.
+ # This ensures a mismatch error where there are multiple top level sources
+ # that contain the same gem with different checksums.
+ def replace(spec, checksum)
+ return if Bundler.settings[:disable_checksum_validation]
return unless checksum
- sums = (store[full_name] ||= [])
- sums.find {|c| c.algo == checksum.algo }&.merge!(checksum) || sums << checksum
- rescue SecurityError => e
- raise e.exception(<<~MESSAGE)
- Bundler found multiple different checksums for #{full_name}.
- This means that there are multiple different `#{full_name}.gem` files.
- This is a potential security issue, since Bundler could be attempting \
- to install a different gem than what you expect.
+ name_tuple = spec.name_tuple
+ checksums = (store[name_tuple] ||= {})
+ existing = checksums[checksum.algo]
- #{e.message}
- To resolve this issue:
- 1. delete any downloaded gems referenced above
- 2. run `bundle install`
+ # we assume only one source because this is used while building the index
+ if !existing || existing.sources.first == checksum.sources.first
+ checksums[checksum.algo] = checksum
+ else
+ register_checksum(name_tuple, checksum)
+ end
+ end
- If you are sure that the new checksum is correct, you can \
- remove the `#{full_name}` entry under the lockfile `CHECKSUMS` \
- section and rerun `bundle install`.
+ def register(spec, checksum)
+ return if Bundler.settings[:disable_checksum_validation]
+ return unless checksum
+ register_checksum(spec.name_tuple, checksum)
+ end
- If you wish to continue installing the downloaded gem, and are certain it does not pose a \
- security issue despite the mismatching checksum, do the following:
- 1. run `bundle config set --local disable_checksum_validation true` to turn off checksum verification
- 2. run `bundle install`
- MESSAGE
+ def merge!(other)
+ other.store.each do |name_tuple, checksums|
+ checksums.each do |_algo, checksum|
+ register_checksum(name_tuple, checksum)
+ end
+ end
end
- def replace(full_name, checksum)
- store[full_name] = checksum ? [checksum] : nil
+ def to_lock(spec)
+ name_tuple = spec.name_tuple
+ if checksums = store[name_tuple]
+ "#{name_tuple.lock_name} #{checksums.values.map(&:to_lock).sort.join(",")}"
+ else
+ name_tuple.lock_name
+ end
end
- def register_store(other)
- other.store.each do |full_name, checksums|
- checksums.each {|checksum| register(full_name, checksum) }
+ private
+
+ def register_checksum(name_tuple, checksum)
+ return unless checksum
+ checksums = (store[name_tuple] ||= {})
+ existing = checksums[checksum.algo]
+
+ if !existing
+ checksums[checksum.algo] = checksum
+ elsif existing.merge!(checksum)
+ checksum
+ else
+ raise ChecksumMismatchError.new(name_tuple, existing, checksum)
end
end
end
diff --git a/lib/bundler/definition.rb b/lib/bundler/definition.rb
index 9d13c18d37..3815a55b04 100644
--- a/lib/bundler/definition.rb
+++ b/lib/bundler/definition.rb
@@ -751,8 +751,8 @@ module Bundler
sources.all_sources.each do |source|
# has to be done separately, because we want to keep the locked checksum
# store for a source, even when doing a full update
- if @locked_gems && locked_source = @locked_gems.sources.find {|s| s == source }
- source.checksum_store.register_store(locked_source.checksum_store)
+ if @locked_gems && locked_source = @locked_gems.sources.find {|s| s == source && !s.equal?(source) }
+ source.checksum_store.merge!(locked_source.checksum_store)
end
# If the source is unlockable and the current command allows an unlock of
# the source (for example, you are doing a `bundle update <foo>` of a git-pinned
diff --git a/lib/bundler/endpoint_specification.rb b/lib/bundler/endpoint_specification.rb
index 11c71d1ae7..0bb0e9c7fa 100644
--- a/lib/bundler/endpoint_specification.rb
+++ b/lib/bundler/endpoint_specification.rb
@@ -126,16 +126,11 @@ module Bundler
case k.to_s
when "checksum"
next if Bundler.settings[:disable_checksum_validation]
- digest = v.last
- if digest.length == 64
- # nothing to do, it's a hexdigest
- elsif digest.length == 44
- # transform the bytes from base64 to hex
- digest = digest.unpack("m0").first.unpack("H*").first
- else
- raise ArgumentError, "The given checksum for #{full_name} (#{digest.inspect}) is not a valid SHA256 hexdigest nor base64digest"
+ begin
+ @checksum = Checksum.from_api(v.last, @spec_fetcher.uri)
+ rescue ArgumentError => e
+ raise ArgumentError, "Invalid checksum for #{full_name}: #{e.message}"
end
- @checksum = Checksum.new("sha256", digest, "API response from #{@spec_fetcher.uri}")
when "rubygems"
@required_rubygems_version = Gem::Requirement.new(v)
when "ruby"
diff --git a/lib/bundler/errors.rb b/lib/bundler/errors.rb
index 5839fc6a73..c6b3cec4dc 100644
--- a/lib/bundler/errors.rb
+++ b/lib/bundler/errors.rb
@@ -52,6 +52,49 @@ module Bundler
class GemfileEvalError < GemfileError; end
class MarshalError < StandardError; end
+ class ChecksumMismatchError < SecurityError
+ def initialize(name_tuple, existing, checksum)
+ @name_tuple = name_tuple
+ @existing = existing
+ @checksum = checksum
+ end
+
+ def message
+ <<~MESSAGE
+ Bundler found mismatched checksums. This is a potential security risk.
+ #{@name_tuple.lock_name} #{@existing.to_lock}
+ from #{@existing.sources.join("\n and ")}
+ #{@name_tuple.lock_name} #{@checksum.to_lock}
+ from #{@checksum.sources.join("\n and ")}
+
+ #{mismatch_resolution_instructions}
+ To ignore checksum security warnings, disable checksum validation with
+ `bundle config set --local disable_checksum_validation true`
+ MESSAGE
+ end
+
+ def mismatch_resolution_instructions
+ removable, remote = [@existing, @checksum].partition(&:removable?)
+ case removable.size
+ when 0
+ msg = +"Mismatched checksums each have an authoritative source:\n"
+ msg << " 1. #{@existing.sources.reject(&:removable?).map(&:to_s).join(" and ")}\n"
+ msg << " 2. #{@checksum.sources.reject(&:removable?).map(&:to_s).join(" and ")}\n"
+ msg << "You may need to alter your Gemfile sources to resolve this issue.\n"
+ when 1
+ msg = +"If you trust #{remote.first.sources.first}, to resolve this issue you can:\n"
+ msg << removable.first.removal_instructions
+ when 2
+ msg = +"To resolve this issue you can either:\n"
+ msg << @checksum.removal_instructions
+ msg << "or if you are sure that the new checksum from #{@checksum.sources.first} is correct:\n"
+ msg << @existing.removal_instructions
+ end
+ end
+
+ status_code(37)
+ end
+
class PermissionError < BundlerError
def initialize(path, permission_type = :write)
@path = path
diff --git a/lib/bundler/fetcher.rb b/lib/bundler/fetcher.rb
index d493ca0064..e5384c5679 100644
--- a/lib/bundler/fetcher.rb
+++ b/lib/bundler/fetcher.rb
@@ -140,11 +140,7 @@ module Bundler
fetch_specs(gem_names).each do |name, version, platform, dependencies, metadata|
spec = if dependencies
EndpointSpecification.new(name, version, platform, self, dependencies, metadata).tap do |es|
- # Duplicate spec.full_names, different spec.original_names
- # index#<< ensures that the last one added wins, so if we're overriding
- # here, make sure to also override the checksum, otherwise downloading the
- # specs (even if that version is completely unused) will cause a SecurityError
- source.checksum_store.replace(es.full_name, es.checksum)
+ source.checksum_store.replace(es, es.checksum)
end
else
RemoteSpecification.new(name, version, platform, self)
diff --git a/lib/bundler/gem_helpers.rb b/lib/bundler/gem_helpers.rb
index ed39511a10..2e6d788f9c 100644
--- a/lib/bundler/gem_helpers.rb
+++ b/lib/bundler/gem_helpers.rb
@@ -113,23 +113,5 @@ module Bundler
same_runtime_deps && same_metadata_deps
end
module_function :same_deps
-
- def spec_full_name(name, version, platform)
- if platform == Gem::Platform::RUBY
- "#{name}-#{version}"
- else
- "#{name}-#{version}-#{platform}"
- end
- end
- module_function :spec_full_name
-
- def lock_name(name, version, platform)
- if platform == Gem::Platform::RUBY
- "#{name} (#{version})"
- else
- "#{name} (#{version}-#{platform})"
- end
- end
- module_function :lock_name
end
end
diff --git a/lib/bundler/lazy_specification.rb b/lib/bundler/lazy_specification.rb
index 2d084e462f..970869e084 100644
--- a/lib/bundler/lazy_specification.rb
+++ b/lib/bundler/lazy_specification.rb
@@ -20,7 +20,19 @@ module Bundler
end
def full_name
- @full_name ||= GemHelpers.spec_full_name(@name, @version, platform)
+ @full_name ||= if platform == Gem::Platform::RUBY
+ "#{@name}-#{@version}"
+ else
+ "#{@name}-#{@version}-#{platform}"
+ end
+ end
+
+ def lock_name
+ @lock_name ||= name_tuple.lock_name
+ end
+
+ def name_tuple
+ Gem::NameTuple.new(@name, @version, @platform)
end
def ==(other)
@@ -57,7 +69,7 @@ module Bundler
def to_lock
out = String.new
- out << " #{GemHelpers.lock_name(name, version, platform)}\n"
+ out << " #{lock_name}\n"
dependencies.sort_by(&:to_s).uniq.each do |dep|
next if dep.type == :development
@@ -113,7 +125,7 @@ module Bundler
end
def to_s
- @to_s ||= GemHelpers.lock_name(name, version, platform)
+ lock_name
end
def git_version
diff --git a/lib/bundler/lockfile_generator.rb b/lib/bundler/lockfile_generator.rb
index 8114c27917..4d2a968d7e 100644
--- a/lib/bundler/lockfile_generator.rb
+++ b/lib/bundler/lockfile_generator.rb
@@ -67,15 +67,10 @@ module Bundler
end
def add_checksums
- out << "\nCHECKSUMS\n"
-
- definition.resolve.sort_by(&:full_name).each do |spec|
- lock_name = GemHelpers.lock_name(spec.name, spec.version, spec.platform)
- out << " #{lock_name}"
- checksums = spec.source.checksum_store.checksums(spec.full_name)
- out << " #{checksums.map(&:to_lock).sort.join(",")}" if checksums
- out << "\n"
+ checksums = definition.resolve.map do |spec|
+ spec.source.checksum_store.to_lock(spec)
end
+ add_section("CHECKSUMS", checksums)
end
def add_locked_ruby_version
diff --git a/lib/bundler/lockfile_parser.rb b/lib/bundler/lockfile_parser.rb
index 9ffe5beffd..2f00da7ea9 100644
--- a/lib/bundler/lockfile_parser.rb
+++ b/lib/bundler/lockfile_parser.rb
@@ -101,7 +101,10 @@ module Bundler
"Run `git checkout HEAD -- #{@lockfile_path}` first to get a clean lock."
end
- lockfile.split(/((?:\r?\n)+)/).each_slice(2) do |line, whitespace|
+ lockfile.split(/((?:\r?\n)+)/) do |line|
+ # split alternates between the line and the following whitespace
+ next @pos.advance!(line) if line.match?(/^\s*$/)
+
if SOURCE.include?(line)
@parse_method = :parse_source
parse_source(line)
@@ -121,7 +124,6 @@ module Bundler
send(@parse_method, line)
end
@pos.advance!(line)
- @pos.advance!(whitespace)
end
@specs = @specs.values.sort_by!(&:full_name)
rescue ArgumentError => e
@@ -217,23 +219,23 @@ module Bundler
spaces = $1
return unless spaces.size == 2
+ checksums = $6
+ return unless checksums
name = $2
version = $3
platform = $4
- checksums = $6
- return unless checksums
version = Gem::Version.new(version)
platform = platform ? Gem::Platform.new(platform) : Gem::Platform::RUBY
- full_name = GemHelpers.spec_full_name(name, version, platform)
+ full_name = Gem::NameTuple.new(name, version, platform).full_name
# Don't raise exception if there's a checksum for a gem that's not in the lockfile,
# we prefer to heal invalid lockfiles
return unless spec = @specs[full_name]
- checksums.split(",").each do |c|
- algo, digest = c.split("-", 2)
- lock_name = GemHelpers.lock_name(spec.name, spec.version, spec.platform)
- spec.source.checksum_store.register(full_name, Checksum.new(algo, digest, "#{@lockfile_path}:#{@pos} CHECKSUMS #{lock_name}"))
+ checksums.split(",") do |lock_checksum|
+ column = line.index(lock_checksum) + 1
+ checksum = Checksum.from_lock(lock_checksum, "#{@lockfile_path}:#{@pos.line}:#{column}")
+ spec.source.checksum_store.register(spec, checksum)
end
end
diff --git a/lib/bundler/rubygems_ext.rb b/lib/bundler/rubygems_ext.rb
index b96edd5e2d..cb131a0185 100644
--- a/lib/bundler/rubygems_ext.rb
+++ b/lib/bundler/rubygems_ext.rb
@@ -359,6 +359,27 @@ module Gem
end
end
+ require "rubygems/name_tuple"
+
+ class NameTuple
+ def self.new(name, version, platform="ruby")
+ if Gem::Platform === platform
+ super(name, version, platform.to_s)
+ else
+ super
+ end
+ end
+
+ def lock_name
+ @lock_name ||=
+ if platform == Gem::Platform::RUBY
+ "#{name} (#{version})"
+ else
+ "#{name} (#{version}-#{platform})"
+ end
+ end
+ end
+
require "rubygems/util"
Util.singleton_class.module_eval do
diff --git a/lib/bundler/rubygems_gem_installer.rb b/lib/bundler/rubygems_gem_installer.rb
index c381956fc3..4d0e1c3fcc 100644
--- a/lib/bundler/rubygems_gem_installer.rb
+++ b/lib/bundler/rubygems_gem_installer.rb
@@ -60,10 +60,6 @@ module Bundler
end
end
- def pre_install_checks
- super && validate_bundler_checksum(options[:bundler_checksum_store])
- end
-
def build_extensions
extension_cache_path = options[:bundler_extension_cache_path]
extension_dir = spec.extension_dir
@@ -98,6 +94,18 @@ module Bundler
end
end
+ def gem_checksum
+ return nil if Bundler.settings[:disable_checksum_validation]
+ return nil unless source = @package.instance_variable_get(:@gem)
+ return nil unless source.respond_to?(:with_read_io)
+
+ source.with_read_io do |io|
+ Checksum.from_gem(io, source.path)
+ ensure
+ io.rewind
+ end
+ end
+
private
def prepare_extension_build(extension_dir)
@@ -114,14 +122,5 @@ module Bundler
raise DirectoryRemovalError.new(e, "Could not delete previous installation of `#{dir}`")
end
-
- def validate_bundler_checksum(checksum_store)
- return true if Bundler.settings[:disable_checksum_validation]
- return true unless source = @package.instance_variable_get(:@gem)
- return true unless source.respond_to?(:with_read_io)
-
- checksum_store.register_gem_package spec, source
- true
- end
end
end
diff --git a/lib/bundler/source/rubygems.rb b/lib/bundler/source/rubygems.rb
index 9ea8367006..6d6f36e298 100644
--- a/lib/bundler/source/rubygems.rb
+++ b/lib/bundler/source/rubygems.rb
@@ -178,7 +178,6 @@ module Bundler
:wrappers => true,
:env_shebang => true,
:build_args => options[:build_args],
- :bundler_checksum_store => spec.source.checksum_store,
:bundler_extension_cache_path => extension_cache_path(spec)
)
@@ -197,6 +196,8 @@ module Bundler
spec.__swap__(s)
end
+ spec.source.checksum_store.register(spec, installer.gem_checksum)
+
message = "Installing #{version_message(spec, options[:previous_spec])}"
message += " with native extensions" if spec.extensions.any?
Bundler.ui.confirm message