CVE-2017-0903: Deserialization of Untrusted Data (CWE-502)
value: RubyGems versions between 2.0.0 and 2.6.13 are vulnerable to a possible remote code execution vulnerability. YAML deserialization of gem specifications can bypass class white lists. Specially crafted serialized objects can possibly be used to escalate to remote code execution.
CWE description: The product deserializes untrusted data without sufficiently verifying that the resulting data will be valid.
CVSS v3 base score: 9.8
Published: 2017-10-11T18:29Z
Repository: https://github.com/rubygems/rubygems
Language: Ruby
Files changed: Manifest.txt, lib/rubygems.rb, lib/rubygems/config_file.rb, lib/rubygems/package.rb, lib/rubygems/package/old.rb, lib/rubygems/safe_yaml.rb, lib/rubygems/specification.rb
Commit date: 2017-10-09 22:01:08 -0500
Commit message: Whitelist classes and symbols that are in Gem spec YAML

This patch adds a method for loading YAML specs from a gem and
whitelists classes and symbols that are allowed in the spec.  Then it
changes calls to YAML.load to call the whitelisted "safe" loader
instead.

[CVE-2017-0903]

Diff: diff --git a/Manifest.txt b/Manifest.txt
index 182ed4c13..c091e7c50 100644
--- a/Manifest.txt
+++ b/Manifest.txt
@@ -385,10 +385,11 @@ lib/rubygems/resolver/source_set.rb
 lib/rubygems/resolver/spec_specification.rb
 lib/rubygems/resolver/specification.rb
 lib/rubygems/resolver/stats.rb
 lib/rubygems/resolver/vendor_set.rb
 lib/rubygems/resolver/vendor_specification.rb
+lib/rubygems/safe_yaml.rb
 lib/rubygems/security.rb
 lib/rubygems/security/policies.rb
 lib/rubygems/security/policy.rb
 lib/rubygems/security/signer.rb
 lib/rubygems/security/trust_dir.rb
diff --git a/lib/rubygems.rb b/lib/rubygems.rb
index 55aa85b8b..a13c6338c 100644
--- a/lib/rubygems.rb
+++ b/lib/rubygems.rb
@@ -673,11 +673,11 @@ def self.load_yaml
     # Only Ruby 1.8 and 1.9 have syck
     test_syck = false unless /^1\./ =~ RUBY_VERSION
 
     unless test_syck
       begin
-        gem 'psych', '>= 1.2.1'
+        gem 'psych', '>= 2.0.0'
       rescue Gem::LoadError
         # It's OK if the user does not have the psych gem installed.  We will
         # attempt to require the stdlib version
       end
 
@@ -697,10 +697,11 @@ def self.load_yaml
         require 'rubygems/psych_tree'
       end
     end
 
     require 'yaml'
+    require 'rubygems/safe_yaml'
 
     # If we're supposed to be using syck, then we may have to force
     # activate it via the YAML::ENGINE API.
     if test_syck and defined?(YAML::ENGINE)
       YAML::ENGINE.yamler = "syck" unless YAML::ENGINE.syck?
diff --git a/lib/rubygems/config_file.rb b/lib/rubygems/config_file.rb
index c95d7dd1f..63583b361 100644
--- a/lib/rubygems/config_file.rb
+++ b/lib/rubygems/config_file.rb
@@ -343,11 +343,11 @@ def load_file(filename)
     yaml_errors << Psych::SyntaxError if defined?(Psych::SyntaxError)
 
     return {} unless filename and File.exist? filename
 
     begin
-      content = YAML.load(File.read(filename))
+      content = Gem::SafeYAML.load(File.read(filename))
       unless content.kind_of? Hash
         warn "Failed to load #{filename} because it doesn't contain valid YAML hash"
         return {}
       end
       return content
diff --git a/lib/rubygems/package.rb b/lib/rubygems/package.rb
index c36e71d80..77811ed5e 100644
--- a/lib/rubygems/package.rb
+++ b/lib/rubygems/package.rb
@@ -466,11 +466,11 @@ def open_tar_gz io # :nodoc:
   def read_checksums gem
     Gem.load_yaml
 
     @checksums = gem.seek 'checksums.yaml.gz' do |entry|
       Zlib::GzipReader.wrap entry do |gz_io|
-        YAML.load gz_io.read
+        Gem::SafeYAML.safe_load gz_io.read
       end
     end
   end
 
   ##
diff --git a/lib/rubygems/package/old.rb b/lib/rubygems/package/old.rb
index 5e722baa3..071f7141a 100644
--- a/lib/rubygems/package/old.rb
+++ b/lib/rubygems/package/old.rb
@@ -99,11 +99,11 @@ def file_list io # :nodoc:
 
     read_until_dashes io do |line|
       header << line
     end
 
-    YAML.load header
+    Gem::SafeYAML.safe_load header
   end
 
   ##
   # Reads lines until a "---" separator is found
 
diff --git a/lib/rubygems/safe_yaml.rb b/lib/rubygems/safe_yaml.rb
new file mode 100644
index 000000000..b98cfaa5e
--- /dev/null
+++ b/lib/rubygems/safe_yaml.rb
@@ -0,0 +1,48 @@
+module Gem
+
+  ###
+  # This module is used for safely loading YAML specs from a gem.  The
+  # `safe_load` method defined on this module is specifically designed for
+  # loading Gem specifications.  For loading other YAML safely, please see
+  # Psych.safe_load
+
+  module SafeYAML
+    WHITELISTED_CLASSES = %w(
+      Symbol
+      Time
+      Date
+      Gem::Dependency
+      Gem::Platform
+      Gem::Requirement
+      Gem::Specification
+      Gem::Version
+      Gem::Version::Requirement
+      YAML::Syck::DefaultKey
+      Syck::DefaultKey
+    )
+
+    WHITELISTED_SYMBOLS = %w(
+      development
+      runtime
+    )
+
+    if ::YAML.respond_to? :safe_load
+      def self.safe_load input
+        ::YAML.safe_load(input, WHITELISTED_CLASSES, WHITELISTED_SYMBOLS, true)
+      end
+
+      def self.load input
+        ::YAML.safe_load(input, [::Symbol])
+      end
+    else
+      warn "YAML safe loading is not available. Please upgrade psych to a version that supports safe loading (>= 2.0)."
+      def self.safe_load input, *args
+        ::YAML.load input
+      end
+
+      def self.load input
+        ::YAML.load input
+      end
+    end
+  end
+end
diff --git a/lib/rubygems/specification.rb b/lib/rubygems/specification.rb
index 8218cd9d4..a9058baf1 100644
--- a/lib/rubygems/specification.rb
+++ b/lib/rubygems/specification.rb
@@ -1099,11 +1099,11 @@ def self.find_in_unresolved_tree path
 
   def self.from_yaml(input)
     Gem.load_yaml
 
     input = normalize_yaml_input input
-    spec = YAML.load input
+    spec = Gem::SafeYAML.safe_load input
 
     if spec && spec.class == FalseClass then
       raise Gem::EndOfYAMLException
     end
 

Vulnerable code:         gem 'psych', '>= 1.2.1'
      content = YAML.load(File.read(filename))
        YAML.load gz_io.read
    YAML.load header
    spec = YAML.load input
Fixed code: lib/rubygems/safe_yaml.rb
        gem 'psych', '>= 2.0.0'
    require 'rubygems/safe_yaml'
      content = Gem::SafeYAML.load(File.read(filename))
        Gem::SafeYAML.safe_load gz_io.read
    Gem::SafeYAML.safe_load header
module Gem

  ###
  # This module is used for safely loading YAML specs from a gem.  The
  # `safe_load` method defined on this module is specifically designed for
  # loading Gem specifications.  For loading other YAML safely, please see
  # Psych.safe_load

  module SafeYAML
    WHITELISTED_CLASSES = %w(
      Symbol
      Time
      Date
      Gem::Dependency
      Gem::Platform
      Gem::Requirement
      Gem::Specification
      Gem::Version
      Gem::Version::Requirement
      YAML::Syck::DefaultKey
      Syck::DefaultKey
    )

    WHITELISTED_SYMBOLS = %w(
      development
      runtime
    )

    if ::YAML.respond_to? :safe_load
      def self.safe_load input
        ::YAML.safe_load(input, WHITELISTED_CLASSES, WHITELISTED_SYMBOLS, true)
      end

      def self.load input
        ::YAML.safe_load(input, [::Symbol])
      end
    else
      warn "YAML safe loading is not available. Please upgrade psych to a version that supports safe loading (>= 2.0)."
      def self.safe_load input, *args
        ::YAML.load input
      end

      def self.load input
        ::YAML.load input
      end
    end
  end
end
    spec = Gem::SafeYAML.safe_load input