From 9ce52a21884ee0f3693d57468d8e5c7ef97934f7 Mon Sep 17 00:00:00 2001 From: Edouard CHIN Date: Thu, 5 Mar 2026 02:31:42 +0100 Subject: [PATCH] Lock the checksum of Bundler itself in the lockfile: - ### Problem With the Bundler autoswitch feature, system Bundler may install a `bundler.gem` that matches the Gemfile.lock. The `bundler.gem` that gets downloaded is like any other gems, but its treated differently (it doesn't appear in the Gemfile specs and we also don't lock its checksum). If for any reason Bundler itself gets compromised, it's a security concern. ### Details I'd like to introduce this change into two separate changes for easier reviews. The first (this commit) only produce the checksum in the lockfile, nothings consumes it or verify it yet. The second patch will make sure that whenever the Bundler auto-install kicks in, Bundler will verify that the locked checksum matches the Bundler version being downloaded and installed. ### Solution Overall the solution here is similar to how checksums are already generated for other gems. However, the `bundler` gem comes from a different source (the `Bundler::Source::Metadata`) and so it needs to be handled slightly differently. A big part ot the change is test related. Instead of having to modify all tests that assert the state of the lockfile (which will be broken now, since the lockfile includes the Bundler checksum), I opted to automatically include the checksum whenever the helper metod `checksums_section` is called. --- bundler/lib/bundler/definition.rb | 2 ++ bundler/lib/bundler/lockfile_generator.rb | 15 ++++++++++++++- bundler/lib/bundler/lockfile_parser.rb | 9 ++++++++- bundler/lib/bundler/source/metadata.rb | 4 ++++ bundler/spec/commands/update_spec.rb | 3 +++ bundler/spec/support/checksums.rb | 4 ++-- 6 files changed, 33 insertions(+), 4 deletions(-) diff --git a/bundler/lib/bundler/definition.rb b/bundler/lib/bundler/definition.rb index efc749e9b326..f312a23f7f2f 100644 --- a/bundler/lib/bundler/definition.rb +++ b/bundler/lib/bundler/definition.rb @@ -988,6 +988,8 @@ def converge_sources end end + sources.metadata_source.checksum_store.merge!(@locked_gems.metadata_source.checksum_store) if @locked_gems + changes end diff --git a/bundler/lib/bundler/lockfile_generator.rb b/bundler/lib/bundler/lockfile_generator.rb index 6b6cf9d9eaee..e23263048c76 100644 --- a/bundler/lib/bundler/lockfile_generator.rb +++ b/bundler/lib/bundler/lockfile_generator.rb @@ -71,7 +71,8 @@ def add_checksums checksums = definition.resolve.map do |spec| spec.source.checksum_store.to_lock(spec) end - add_section("CHECKSUMS", checksums) + + add_section("CHECKSUMS", checksums + bundler_checksum) end def add_locked_ruby_version @@ -100,5 +101,17 @@ def add_section(name, value) raise ArgumentError, "#{value.inspect} can't be serialized in a lockfile" end end + + def bundler_checksum + return [] if Bundler.gem_version.to_s.end_with?(".dev") + + require "rubygems/package" + + bundler_spec = definition.sources.metadata_source.specs.search(["bundler", Bundler.gem_version]).last + package = Gem::Package.new(bundler_spec.cache_file) + definition.sources.metadata_source.checksum_store.register(bundler_spec, Checksum.from_gem_package(package)) + + [definition.sources.metadata_source.checksum_store.to_lock(bundler_spec)] + end end end diff --git a/bundler/lib/bundler/lockfile_parser.rb b/bundler/lib/bundler/lockfile_parser.rb index ac0ce1ef3d0a..a837f994cd90 100644 --- a/bundler/lib/bundler/lockfile_parser.rb +++ b/bundler/lib/bundler/lockfile_parser.rb @@ -28,6 +28,7 @@ def to_s attr_reader( :sources, + :metadata_source, :dependencies, :specs, :platforms, @@ -97,6 +98,7 @@ def self.bundled_with def initialize(lockfile, strict: false) @platforms = [] @sources = [] + @metadata_source = Source::Metadata.new @dependencies = {} @parse_method = nil @specs = {} @@ -252,7 +254,12 @@ def parse_checksum(line) version = Gem::Version.new(version) platform = platform ? Gem::Platform.new(platform) : Gem::Platform::RUBY full_name = Gem::NameTuple.new(name, version, platform).full_name - return unless spec = @specs[full_name] + spec = @specs[full_name] + + if name == "bundler" + spec ||= LazySpecification.new(name, version, platform, @metadata_source) + end + return unless spec if checksums checksums.split(",") do |lock_checksum| diff --git a/bundler/lib/bundler/source/metadata.rb b/bundler/lib/bundler/source/metadata.rb index fd959cd64ee2..ecf889518715 100644 --- a/bundler/lib/bundler/source/metadata.rb +++ b/bundler/lib/bundler/source/metadata.rb @@ -58,6 +58,10 @@ def hash def version_message(spec) "#{spec.name} #{spec.version}" end + + def checksum_store + @checksum_store ||= Checksum::Store.new + end end end end diff --git a/bundler/spec/commands/update_spec.rb b/bundler/spec/commands/update_spec.rb index cdaeb75c4a33..3fc6bd38e41a 100644 --- a/bundler/spec/commands/update_spec.rb +++ b/bundler/spec/commands/update_spec.rb @@ -1537,6 +1537,7 @@ checksums = checksums_section do |c| c.checksum(gem_repo4, "myrack", "1.0") + c.checksum(gem_repo4, "bundler", "999.0.0") end install_gemfile <<-G @@ -1621,6 +1622,7 @@ checksums = checksums_section do |c| c.checksum(gem_repo4, "myrack", "1.0") + c.checksum(gem_repo4, "bundler", "9.9.9") end install_gemfile <<-G @@ -1745,6 +1747,7 @@ # Only updates properly on modern RubyGems. checksums = checksums_section_when_enabled do |c| c.checksum(gem_repo4, "myrack", "1.0") + c.checksum(local_gem_path, "bundler", "9.0.0", Gem::Platform::RUBY, "cache") end expect(lockfile).to eq <<~L diff --git a/bundler/spec/support/checksums.rb b/bundler/spec/support/checksums.rb index cf8ea417d67a..03147bf6f463 100644 --- a/bundler/spec/support/checksums.rb +++ b/bundler/spec/support/checksums.rb @@ -14,9 +14,9 @@ def initialize_copy(original) @checksums = @checksums.dup end - def checksum(repo, name, version, platform = Gem::Platform::RUBY) + def checksum(repo, name, version, platform = Gem::Platform::RUBY, folder = "gems") name_tuple = Gem::NameTuple.new(name, version, platform) - gem_file = File.join(repo, "gems", "#{name_tuple.full_name}.gem") + gem_file = File.join(repo, folder, "#{name_tuple.full_name}.gem") File.open(gem_file, "rb") do |f| register(name_tuple, Bundler::Checksum.from_gem(f, "#{gem_file} (via ChecksumsBuilder#checksum)")) end