summaryrefslogtreecommitdiff
path: root/vendor/gems/bundler-checksum/lib/bundler_checksum.rb
blob: b3d36521f241016427528abb0fe5a275cb75c302 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# frozen_string_literal: true

require 'bundler'
require 'bundler_checksum/version'
require 'json'

module Bundler
  module Patches
    # This module monkey-patches Bundler to check Gemfile.checksum
    # when installing gems that are from RubyGems
    module RubyGemsInstallerPatch
      def pre_install_checks
        super && validate_local_package_checksum
      end

      private

      def validate_local_package_checksum
        cached_checksum = fetch_checksum_from_file(spec)

        if cached_checksum.nil?
          raise SecurityError, "Cached checksum for #{spec.full_name} not found. Please (re-)generate Gemfile.checksum with " \
            "`bundle exec bundler-checksum init`. See https://docs.gitlab.com/ee/development/gemfile.html#updating-the-checksum-file."
        end

        validate_file_checksum(cached_checksum)
      end

      def fetch_checksum_from_file(spec)
        ::BundlerChecksum.checksum_for(spec.name, spec.version.to_s, spec.platform.to_s)
      end

      # Modified from
      # https://github.com/rubygems/rubygems/blob/243173279e79a38f03e318eea8825d1c8824e119/bundler/lib/bundler/rubygems_gem_installer.rb#L116
      def validate_file_checksum(checksum)
        return true if Bundler.settings[:disable_checksum_validation]

        source = @package.instance_variable_get(:@gem)

        # Contary to upstream, we raise instead of silently returning
        raise "#{@package.inspect} does not have :@gem" unless source
        raise "#{source.inspect} does not respond to :with_read_io" unless source.respond_to?(:with_read_io)

        digest = source.with_read_io do |io|
          digest = SharedHelpers.digest(:SHA256).new
          digest << io.read(16_384) until io.eof?
          io.rewind
          send(checksum_type(checksum), digest)
        end
        unless digest == checksum
          raise SecurityError, <<-MESSAGE
          Bundler cannot continue installing #{spec.name} (#{spec.version}).
          The checksum for the downloaded `#{spec.full_name}.gem` does not match \
          the checksum from the checksum file. This means the contents of the downloaded \
          gem is different from what was recorded in the checksum file, and could be potential security issue.
          gem is different from what was uploaded to the server, and could be a potential security issue.

          To resolve this issue:
          1. delete the downloaded gem located at: `#{spec.gem_dir}/#{spec.full_name}.gem`
          2. run `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`

          (More info: The expected SHA256 checksum was #{checksum.inspect}, but the \
          checksum for the downloaded gem was #{digest.inspect}.)
          MESSAGE
        end
        true
      end
    end
  end
end

module BundlerChecksum
  class << self
    def checksum_file
      @checksum_file ||= File.join(File.dirname(Bundler.default_gemfile), 'Gemfile.checksum')
    end

    def checksums_from_file
      @checksums_from_file ||= JSON.parse(File.open(checksum_file).read, symbolize_names: true)
    rescue JSON::ParserError => e
      raise "Invalid checksum file: #{e.message}"
    end

    def checksum_for(gem_name, gem_version, gem_platform)
      item = checksums_from_file.detect do |item|
        item[:name] == gem_name &&
          item[:platform] == gem_platform &&
          item[:version] == gem_version
      end

      item&.fetch(:checksum)
    end

    def patch!
      return if defined?(@patched) && @patched
      @patched = true

      Bundler.ui.info "Patching bundler with bundler-checksum..."
      require 'bundler/rubygems_gem_installer'
      ::Bundler::RubyGemsGemInstaller.prepend(Bundler::Patches::RubyGemsInstallerPatch)
    end
  end
end