CVE-2020-4054: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: In Sanitize (RubyGem sanitize) greater than or equal to 3.0.0 and less than 5.2.1, there is a cross-site scripting vulnerability. When HTML is sanitized using Sanitize's "relaxed" config, or a custom config that allows certain elements, some content in a math or svg element may not be sanitized correctly even if math and svg are not in the allowlist. You are likely to be vulnerable to this issue if you use Sanitize's relaxed config or a custom config that allows one or more of the following HTML elements: iframe, math, noembed, noframes, noscript, plaintext, script, style, svg, xmp. Using carefully crafted input, an attacker may be able to sneak arbitrary HTML through Sanitize, potentially resulting in XSS (cross-site scripting) or other undesired behavior when that HTML is rendered in a browser. This has been fixed in 5.2.1.
CWE description: The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
CVSS v3 base score: 7.3
Published: 2020-06-16T22:15Z
Repository: https://github.com/rgrove/sanitize
Language: Ruby
Files changed: README.md, lib/sanitize/config/default.rb, test/test_clean_element.rb, test/test_malicious_html.rb
Commit date: 2020-06-15 17:27:27 -0700
Commit message: Fix sanitization bypass in HTML foreign content

https://github.com/rgrove/sanitize/security/advisories/GHSA-p4x4-rw2p-8j8m

Diff: diff --git a/README.md b/README.md
index 86cd366..d867e4a 100644
--- a/README.md
+++ b/README.md
@@ -70,10 +70,15 @@ Sanitize can sanitize the following types of input:
 * CSS stylesheets inside HTML `<style>` elements
 * CSS properties inside HTML `style` attributes
 * Standalone CSS stylesheets
 * Standalone CSS properties
 
+However, please note that Sanitize _cannot_ fully sanitize the contents of
+`<math>` or `<svg>` elements, since these elements don't follow the same parsing
+rules as the rest of HTML. If this is something you need, you may want to look
+for another solution.
+
 ### HTML Fragments
 
 A fragment is a snippet of HTML that doesn't contain a root-level `<html>`
 element.
 
@@ -413,10 +418,16 @@ elements not in this array will be removed.
   a abbr b blockquote br cite code dd dfn dl dt em i kbd li mark ol p pre
   q s samp small strike strong sub sup time u ul var
 ]
 ```
 
+**Warning:** Sanitize cannot fully sanitize the contents of `<math>` or `<svg>`
+elements, since these elements don't follow the same parsing rules as the rest
+of HTML. If you add `math` or `svg` to the allowlist, you must assume that any
+content inside them will be allowed, even if that content would otherwise be
+removed by Sanitize.
+
 #### :parser_options (Hash)
 
 [Parsing options](https://github.com/rubys/nokogumbo/tree/v2.0.1#parsing-options) supplied to `nokogumbo`.
 
 ```ruby
diff --git a/lib/sanitize/config/default.rb b/lib/sanitize/config/default.rb
index 9cee03e..4dd1d22 100644
--- a/lib/sanitize/config/default.rb
+++ b/lib/sanitize/config/default.rb
@@ -72,11 +72,11 @@ class Sanitize
       #
       # If this is an Array or Set of element names, then only the contents of
       # the specified elements (when filtered) will be removed, and the contents
       # of all other filtered elements will be left behind.
       :remove_contents => %w[
-        iframe noembed noframes noscript script style
+        iframe math noembed noframes noscript plaintext script style svg xmp
       ],
 
       # Transformers allow you to filter or alter nodes using custom logic. See
       # README.md for details and examples.
       :transformers => [],
diff --git a/test/test_clean_element.rb b/test/test_clean_element.rb
index ba0c030..37a8af0 100644
--- a/test/test_clean_element.rb
+++ b/test/test_clean_element.rb
@@ -190,25 +190,20 @@ describe 'Sanitize::Transformers::CleanElement' do
     it 'should not choke on several instances of the same element in a row' do
       Sanitize.fragment('<img src="http://www.google.com/intl/en_ALL/images/logo.gif"><img src="http://www.google.com/intl/en_ALL/images/logo.gif"><img src="http://www.google.com/intl/en_ALL/images/logo.gif"><img src="http://www.google.com/intl/en_ALL/images/logo.gif">')
         .must_equal ''
     end
 
-    it 'should escape the content of removed `plaintext` elements' do
-      Sanitize.fragment('<plaintext>hello! <script>alert(0)</script>')
-        .must_equal 'hello! &lt;script&gt;alert(0)&lt;/script&gt;'
-    end
-
-    it 'should escape the content of removed `xmp` elements' do
-      Sanitize.fragment('<xmp>hello! <script>alert(0)</script></xmp>')
-        .must_equal 'hello! &lt;script&gt;alert(0)&lt;/script&gt;'
-    end
-
     it 'should not preserve the content of removed `iframe` elements' do
       Sanitize.fragment('<iframe>hello! <script>alert(0)</script></iframe>')
         .must_equal ''
     end
 
+    it 'should not preserve the content of removed `math` elements' do
+      Sanitize.fragment('<math>hello! <script>alert(0)</script></math>')
+        .must_equal ''
+    end
+
     it 'should not preserve the content of removed `noembed` elements' do
       Sanitize.fragment('<noembed>hello! <script>alert(0)</script></noembed>')
         .must_equal ''
     end
 
@@ -220,20 +215,35 @@ describe 'Sanitize::Transformers::CleanElement' do
     it 'should not preserve the content of removed `noscript` elements' do
       Sanitize.fragment('<noscript>hello! <script>alert(0)</script></noscript>')
         .must_equal ''
     end
 
+    it 'should not preserve the content of removed `plaintext` elements' do
+      Sanitize.fragment('<plaintext>hello! <script>alert(0)</script>')
+        .must_equal ''
+    end
+
     it 'should not preserve the content of removed `script` elements' do
       Sanitize.fragment('<script>hello! <script>alert(0)</script></script>')
         .must_equal ''
     end
 
     it 'should not preserve the content of removed `style` elements' do
       Sanitize.fragment('<style>hello! <script>alert(0)</script></style>')
         .must_equal ''
     end
 
+    it 'should not preserve the content of removed `svg` elements' do
+      Sanitize.fragment('<svg>hello! <script>alert(0)</script></svg>')
+        .must_equal ''
+    end
+
+    it 'should not preserve the content of removed `xmp` elements' do
+      Sanitize.fragment('<xmp>hello! <script>alert(0)</script></xmp>')
+        .must_equal ''
+    end
+
     strings.each do |name, data|
       it "should clean #{name} HTML" do
         Sanitize.fragment(data[:html]).must_equal(data[:default])
       end
     end
diff --git a/test/test_malicious_html.rb b/test/test_malicious_html.rb
index 945978f..39163b9 100644
--- a/test/test_malicious_html.rb
+++ b/test/test_malicious_html.rb
@@ -217,6 +217,19 @@ describe 'Malicious HTML' do
           @s.fragment(output).must_equal(output)
         end
       end
     end
   end
+
+  # https://github.com/rgrove/sanitize/security/advisories/GHSA-p4x4-rw2p-8j8m
+  describe 'foreign content bypass in relaxed config' do
+    it 'prevents a sanitization bypass via carefully crafted foreign content' do
+      %w[iframe noembed noframes noscript plaintext script style xmp].each do |tag_name|
+        @s.fragment(%[<math><#{tag_name}>/*&lt;/#{tag_name}&gt;&lt;img src onerror=alert(1)>*/]).
+          must_equal ''
+
+        @s.fragment(%[<svg><#{tag_name}>/*&lt;/#{tag_name}&gt;&lt;img src onerror=alert(1)>*/]).
+          must_equal ''
+      end
+    end
+  end
 end

Vulnerable code:         iframe noembed noframes noscript script style
    it 'should escape the content of removed `plaintext` elements' do
      Sanitize.fragment('<plaintext>hello! <script>alert(0)</script>')
        .must_equal 'hello! &lt;script&gt;alert(0)&lt;/script&gt;'
    end

    it 'should escape the content of removed `xmp` elements' do
      Sanitize.fragment('<xmp>hello! <script>alert(0)</script></xmp>')
        .must_equal 'hello! &lt;script&gt;alert(0)&lt;/script&gt;'
    end

Fixed code: However, please note that Sanitize _cannot_ fully sanitize the contents of
`<math>` or `<svg>` elements, since these elements don't follow the same parsing
rules as the rest of HTML. If this is something you need, you may want to look
for another solution.

**Warning:** Sanitize cannot fully sanitize the contents of `<math>` or `<svg>`
elements, since these elements don't follow the same parsing rules as the rest
of HTML. If you add `math` or `svg` to the allowlist, you must assume that any
content inside them will be allowed, even if that content would otherwise be
removed by Sanitize.

        iframe math noembed noframes noscript plaintext script style svg xmp
    it 'should not preserve the content of removed `math` elements' do
      Sanitize.fragment('<math>hello! <script>alert(0)</script></math>')
        .must_equal ''
    end

    it 'should not preserve the content of removed `plaintext` elements' do
      Sanitize.fragment('<plaintext>hello! <script>alert(0)</script>')
        .must_equal ''
    end

    it 'should not preserve the content of removed `svg` elements' do
      Sanitize.fragment('<svg>hello! <script>alert(0)</script></svg>')
        .must_equal ''
    end

    it 'should not preserve the content of removed `xmp` elements' do
      Sanitize.fragment('<xmp>hello! <script>alert(0)</script></xmp>')
        .must_equal ''
    end


  # https://github.com/rgrove/sanitize/security/advisories/GHSA-p4x4-rw2p-8j8m
  describe 'foreign content bypass in relaxed config' do
    it 'prevents a sanitization bypass via carefully crafted foreign content' do
      %w[iframe noembed noframes noscript plaintext script style xmp].each do |tag_name|
        @s.fragment(%[<math><#{tag_name}>/*&lt;/#{tag_name}&gt;&lt;img src onerror=alert(1)>*/]).
          must_equal ''

        @s.fragment(%[<svg><#{tag_name}>/*&lt;/#{tag_name}&gt;&lt;img src onerror=alert(1)>*/]).
          must_equal ''
      end
    end
  end