CVE-2017-0900: Improper Input Validation (CWE-20)
value: RubyGems version 2.6.12 and earlier is vulnerable to maliciously crafted gem specifications to cause a denial of service attack against RubyGems clients who have issued a `query` command.
CWE description: The product receives input or data, but it does
        not validate or incorrectly validates that the input has the
        properties that are required to process the data safely and
        correctly.
CVSS v3 base score: 7.5
Published: 2017-08-31T20:29Z
Repository: https://github.com/rubygems/rubygems
Language: Ruby
Files changed: lib/rubygems/commands/query_command.rb, lib/rubygems/text.rb, test/rubygems/test_gem_commands_query_command.rb, test/rubygems/test_gem_text.rb
Commit date: 2017-08-27 20:37:15 -0500
Commit message: Truncate summaries to 100,000 characters in the query command

This avoids a DOS vector where incredibly large summary strings would hang rubygems due to taking forever in Gem::Text.clean_text

Diff: diff --git a/lib/rubygems/commands/query_command.rb b/lib/rubygems/commands/query_command.rb
index 4ed957b94..44144203e 100644
--- a/lib/rubygems/commands/query_command.rb
+++ b/lib/rubygems/commands/query_command.rb
@@ -351,9 +351,10 @@ def spec_platforms entry, platforms
       end
     end
   end
 
   def spec_summary entry, spec
-    entry << "\n\n" << format_text(spec.summary, 68, 4)
+    summary = truncate_text(spec.summary, "the summary for #{spec.full_name}")
+    entry << "\n\n" << format_text(summary, 68, 4)
   end
 
 end
diff --git a/lib/rubygems/text.rb b/lib/rubygems/text.rb
index 19c78eff1..b944b62c2 100644
--- a/lib/rubygems/text.rb
+++ b/lib/rubygems/text.rb
@@ -11,10 +11,16 @@ module Gem::Text
   # printing.
   def clean_text(text)
     text.gsub(/[\000-\b\v-\f\016-\037\177]/, ".".freeze)
   end
 
+  def truncate_text(text, description, max_length = 100_000)
+    raise ArgumentError, "max_length must be positive" unless max_length > 0
+    return text if text.size <= max_length
+    "Truncating #{description} to #{max_length.to_s.reverse.gsub(/...(?=.)/,'\&,').reverse} characters:\n" + text[0, max_length]
+  end
+
   ##
   # Wraps +text+ to +wrap+ characters and optionally indents by +indent+
   # characters
 
   def format_text(text, wrap, indent=0)
diff --git a/test/rubygems/test_gem_commands_query_command.rb b/test/rubygems/test_gem_commands_query_command.rb
index d55b56555..469223c6c 100644
--- a/test/rubygems/test_gem_commands_query_command.rb
+++ b/test/rubygems/test_gem_commands_query_command.rb
@@ -154,10 +154,50 @@ def test_execute_details_cleans_text
     Homepage: http://a.example.com/.
 
     This is a lot of text. This is a lot of text. This is a lot of text.
     This is a lot of text.
 
+pl (1)
+    Platform: i386-linux
+    Author: A User
+    Homepage: http://example.com
+
+    this is a summary
+    EOF
+
+    assert_equal expected, @ui.output
+    assert_equal '', @ui.error
+  end
+
+  def test_execute_details_truncates_summary
+    spec_fetcher do |fetcher|
+      fetcher.spec 'a', 2 do |s|
+        s.summary = 'This is a lot of text. ' * 10_000
+        s.authors = ["Abraham Lincoln \x01", "\x02 Hirohito"]
+        s.homepage = "http://a.example.com/\x03"
+      end
+
+      fetcher.legacy_platform
+    end
+
+    @cmd.handle_options %w[-r -d]
+
+    use_ui @ui do
+      @cmd.execute
+    end
+
+    expected = <<-EOF
+
+*** REMOTE GEMS ***
+
+a (2)
+    Authors: Abraham Lincoln ., . Hirohito
+    Homepage: http://a.example.com/.
+
+    Truncating the summary for a-2 to 100,000 characters:
+#{"    This is a lot of text. This is a lot of text. This is a lot of text.\n" * 1449}    This is a lot of te
+
 pl (1)
     Platform: i386-linux
     Author: A User
     Homepage: http://example.com
 
diff --git a/test/rubygems/test_gem_text.rb b/test/rubygems/test_gem_text.rb
index 4168931e0..04f3f605e 100644
--- a/test/rubygems/test_gem_text.rb
+++ b/test/rubygems/test_gem_text.rb
@@ -76,6 +76,13 @@ def test_levenshtein_distance_remove
   def test_levenshtein_distance_replace
     assert_equal 2, levenshtein_distance("zentest", "ZenTest")
     assert_equal 7, levenshtein_distance("xxxxxxx", "ZenTest")
     assert_equal 7, levenshtein_distance("zentest", "xxxxxxx")
   end
+
+  def test_truncate_text
+    assert_equal "abc", truncate_text("abc", "desc")
+    assert_equal "Truncating desc to 2 characters:\nab", truncate_text("abc", "desc", 2)
+    s = "ab" * 500_001
+    assert_equal "Truncating desc to 1,000,000 characters:\n#{s[0, 1_000_000]}", truncate_text(s, "desc", 1_000_000)
+  end
 end

Vulnerable code:     entry << "\n\n" << format_text(spec.summary, 68, 4)
Fixed code:     summary = truncate_text(spec.summary, "the summary for #{spec.full_name}")
    entry << "\n\n" << format_text(summary, 68, 4)
  def truncate_text(text, description, max_length = 100_000)
    raise ArgumentError, "max_length must be positive" unless max_length > 0
    return text if text.size <= max_length
    "Truncating #{description} to #{max_length.to_s.reverse.gsub(/...(?=.)/,'\&,').reverse} characters:\n" + text[0, max_length]
  end

pl (1)
    Platform: i386-linux
    Author: A User
    Homepage: http://example.com

    this is a summary
    EOF

    assert_equal expected, @ui.output
    assert_equal '', @ui.error
  end

  def test_execute_details_truncates_summary
    spec_fetcher do |fetcher|
      fetcher.spec 'a', 2 do |s|
        s.summary = 'This is a lot of text. ' * 10_000
        s.authors = ["Abraham Lincoln \x01", "\x02 Hirohito"]
        s.homepage = "http://a.example.com/\x03"
      end

      fetcher.legacy_platform
    end

    @cmd.handle_options %w[-r -d]

    use_ui @ui do
      @cmd.execute
    end

    expected = <<-EOF

*** REMOTE GEMS ***

a (2)
    Authors: Abraham Lincoln ., . Hirohito
    Homepage: http://a.example.com/.

    Truncating the summary for a-2 to 100,000 characters:
#{"    This is a lot of text. This is a lot of text. This is a lot of text.\n" * 1449}    This is a lot of te


  def test_truncate_text
    assert_equal "abc", truncate_text("abc", "desc")
    assert_equal "Truncating desc to 2 characters:\nab", truncate_text("abc", "desc", 2)
    s = "ab" * 500_001
    assert_equal "Truncating desc to 1,000,000 characters:\n#{s[0, 1_000_000]}", truncate_text(s, "desc", 1_000_000)
  end