CVE-2018-1000076: Improper Verification of Cryptographic Signature (CWE-347)
value: RubyGems version Ruby 2.2 series: 2.2.9 and earlier, Ruby 2.3 series: 2.3.6 and earlier, Ruby 2.4 series: 2.4.3 and earlier, Ruby 2.5 series: 2.5.0 and earlier, prior to trunk revision 62422 contains a Improper Verification of Cryptographic Signature vulnerability in package.rb that can result in a mis-signed gem could be installed, as the tarball would contain multiple gem signatures.. This vulnerability appears to have been fixed in 2.7.6.
CWE description: The product does not verify, or incorrectly verifies, the cryptographic signature for data.
CVSS v3 base score: 9.8
Published: 2018-03-13T15:29Z
Repository: https://github.com/rubygems/rubygems
Language: Ruby
Files changed: lib/rubygems/package.rb, lib/rubygems/package/tar_writer.rb, test/rubygems/test_gem_package.rb
Commit date: 2018-02-15 22:19:03 -0800
Commit message: Raise a security error when there are duplicate files in a package

This is a rudimentary fix for an issue where RubyGems would allow a
mis-signed gem to be installed, as the tarball would contain multiple
gem signatures.

Nothing should give us a tarball with multiple entries, so we'll just
disallow that.

Diff: diff --git a/lib/rubygems/package.rb b/lib/rubygems/package.rb
index 9d78544f92..dede959981 100644
--- a/lib/rubygems/package.rb
+++ b/lib/rubygems/package.rb
@@ -601,10 +601,14 @@ def verify_files gem
 
     unless @files.include? 'data.tar.gz' then
       raise Gem::Package::FormatError.new \
               'package content (data.tar.gz) is missing', @gem
     end
+
+    if duplicates = @files.group_by {|f| f }.select {|k,v| v.size > 1 }.map(&:first) and duplicates.any?
+      raise Gem::Security::Exception, "duplicate files in the package: (#{duplicates.map(&:inspect).join(', ')})"
+    end
   end
 
   ##
   # Verifies that +entry+ is a valid gzipped file.
 
diff --git a/lib/rubygems/package/tar_writer.rb b/lib/rubygems/package/tar_writer.rb
index f68b8d4c5e..390f7851a3 100644
--- a/lib/rubygems/package/tar_writer.rb
+++ b/lib/rubygems/package/tar_writer.rb
@@ -194,10 +194,12 @@ def add_file_signed name, mode, signer
         end
 
       digest_name == signer.digest_name
     end
 
+    raise "no #{signer.digest_name} in #{digests.values.compact}" unless signature_digest
+
     if signer.key then
       signature = signer.sign signature_digest.digest
 
       add_file_simple "#{name}.sig", 0444, signature.length do |io|
         io.write signature
diff --git a/test/rubygems/test_gem_package.rb b/test/rubygems/test_gem_package.rb
index be84f7fe25..6ae05fc339 100644
--- a/test/rubygems/test_gem_package.rb
+++ b/test/rubygems/test_gem_package.rb
@@ -736,10 +736,36 @@ def test_verify_nonexistent
 
     assert_match %r%^No such file or directory%, e.message
     assert_match %r%nonexistent.gem$%,           e.message
   end
 
+  def test_verify_duplicate_file
+    FileUtils.mkdir_p 'lib'
+    FileUtils.touch 'lib/code.rb'
+
+    build = Gem::Package.new @gem
+    build.spec = @spec
+    build.setup_signer
+    open @gem, 'wb' do |gem_io|
+      Gem::Package::TarWriter.new gem_io do |gem|
+        build.add_metadata gem
+        build.add_contents gem
+
+        gem.add_file_simple 'a.sig', 0444, 0
+        gem.add_file_simple 'a.sig', 0444, 0
+      end
+    end
+
+    package = Gem::Package.new @gem
+
+    e = assert_raises Gem::Security::Exception do
+      package.verify
+    end
+
+    assert_equal 'duplicate files in the package: ("a.sig")', e.message
+  end
+
   def test_verify_security_policy
     skip 'openssl is missing' unless defined?(OpenSSL::SSL)
 
     package = Gem::Package.new @gem
     package.security_policy = Gem::Security::HighSecurity
@@ -793,11 +819,17 @@ def test_verify_security_policy_checksum_missing
         build.add_metadata gem
         build.add_contents gem
 
         # write bogus data.tar.gz to foil signature
         bogus_data = Gem.gzip 'hello'
-        gem.add_file_simple 'data.tar.gz', 0444, bogus_data.length do |io|
+        fake_signer = Class.new do
+          def digest_name; 'SHA512'; end
+          def digest_algorithm; Digest(:SHA512); end
+          def key; 'key'; end
+          def sign(*); 'fake_sig'; end
+        end
+        gem.add_file_signed 'data2.tar.gz', 0444, fake_signer.new do |io|
           io.write bogus_data
         end
 
         # pre rubygems 2.0 gems do not add checksums
       end

Vulnerable code:         gem.add_file_simple 'data.tar.gz', 0444, bogus_data.length do |io|
Fixed code: 
    if duplicates = @files.group_by {|f| f }.select {|k,v| v.size > 1 }.map(&:first) and duplicates.any?
      raise Gem::Security::Exception, "duplicate files in the package: (#{duplicates.map(&:inspect).join(', ')})"
    end
    raise "no #{signer.digest_name} in #{digests.values.compact}" unless signature_digest

  def test_verify_duplicate_file
    FileUtils.mkdir_p 'lib'
    FileUtils.touch 'lib/code.rb'

    build = Gem::Package.new @gem
    build.spec = @spec
    build.setup_signer
    open @gem, 'wb' do |gem_io|
      Gem::Package::TarWriter.new gem_io do |gem|
        build.add_metadata gem
        build.add_contents gem

        gem.add_file_simple 'a.sig', 0444, 0
        gem.add_file_simple 'a.sig', 0444, 0
      end
    end

    package = Gem::Package.new @gem

    e = assert_raises Gem::Security::Exception do
      package.verify
    end

    assert_equal 'duplicate files in the package: ("a.sig")', e.message
  end

        fake_signer = Class.new do
          def digest_name; 'SHA512'; end
          def digest_algorithm; Digest(:SHA512); end
          def key; 'key'; end
          def sign(*); 'fake_sig'; end
        end
        gem.add_file_signed 'data2.tar.gz', 0444, fake_signer.new do |io|