CVE-2017-1002201: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: In haml versions prior to version 5.0.0.beta.2, when using user input to perform tasks on the server, characters like < > " ' must be escaped properly. In this case, the ' character was missed. An attacker can manipulate the input to introduce additional attributes, potentially executing code.
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: 6.1
Published: 2019-10-15T18:15Z
Repository: https://github.com/haml/haml
Language: Ruby
Files changed: lib/haml/helpers.rb, test/engine_test.rb, test/pretty_engine_test.rb, test/pretty_results/just_stuff.xhtml, test/results/just_stuff.xhtml
Commit date: 2017-02-09 00:29:57 +0900
Commit message: Always escape `'` in Haml::Helpers.#html_escape.

In some tests, `attr_wrapper` is changed to be ignored.

I'll split its fix to other commis because not Haml::Helpers but
Haml::AttributeBuilder is responsible for that.

Diff: diff --git a/lib/haml/helpers.rb b/lib/haml/helpers.rb
index aa26c4c4..48c585cb 100644
--- a/lib/haml/helpers.rb
+++ b/lib/haml/helpers.rb
@@ -594,11 +594,11 @@ MESSAGE
     end
 
     # Characters that need to be escaped to HTML entities from user input
     HTML_ESCAPE = { '&' => '&amp;', '<' => '&lt;', '>' => '&gt;', '"' => '&quot;', "'" => '&#039;' }
 
-    HTML_ESCAPE_REGEX = /[\"><&]/
+    HTML_ESCAPE_REGEX = /['"><&]/
 
     # Returns a copy of `text` with ampersands, angle brackets and quotes
     # escaped into HTML entities.
     #
     # Note that if ActionView is loaded and XSS protection is enabled
diff --git a/test/engine_test.rb b/test/engine_test.rb
index 777790ff..73e7abaa 100644
--- a/test/engine_test.rb
+++ b/test/engine_test.rb
@@ -1125,12 +1125,12 @@ HAML
   end
 
   def test_attr_wrapper
     assert_equal("<p strange=*attrs*></p>\n", render("%p{ :strange => 'attrs'}", :attr_wrapper => '*'))
     assert_equal("<p escaped='quo\"te'></p>\n", render("%p{ :escaped => 'quo\"te'}", :attr_wrapper => '"'))
-    assert_equal("<p escaped=\"quo'te\"></p>\n", render("%p{ :escaped => 'quo\\'te'}", :attr_wrapper => '"'))
-    assert_equal("<p escaped=\"q'uo&#x0022;te\"></p>\n", render("%p{ :escaped => 'q\\'uo\"te'}", :attr_wrapper => '"'))
+    assert_equal("<p escaped=\"quo&#039;te\"></p>\n", render("%p{ :escaped => 'quo\\'te'}", :attr_wrapper => '"'))
+    assert_equal("<p escaped='q&#039;uo\"te'></p>\n", render("%p{ :escaped => 'q\\'uo\"te'}", :attr_wrapper => '"'))
     assert_equal("<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n", render("!!! XML", :attr_wrapper => '"', :format => :xhtml))
   end
 
   def test_autoclose_option
     assert_equal("<flaz foo='bar'>\n", render("%flaz{:foo => 'bar'}", :autoclose => ["flaz"]))
@@ -1532,11 +1532,11 @@ HAML
 
     assert_equal("<div data-one_plus_one='2'></div>\n",
       render("%div{:data => {:one_plus_one => 1+1}}",
         :hyphenate_data_attrs => false))
 
-    assert_equal("<div data-foo='Here&#x0027;s a \"quoteful\" string.'></div>\n",
+    assert_equal("<div data-foo='Here&#039;s a \"quoteful\" string.'></div>\n",
       render(%{%div{:data => {:foo => %{Here's a "quoteful" string.}}}},
         :hyphenate_data_attrs => false)) #'
   end
 
   def test_html5_data_attributes_with_hyphens
@@ -1696,13 +1696,13 @@ HAML
   end
 
   def test_new_attribute_parsing
     assert_equal("<a a2='b2'>bar</a>\n", render("%a(a2=b2) bar", :locals => {:b2 => 'b2'}))
     assert_equal(%Q{<a a='foo"bar'>bar</a>\n}, render(%q{%a(a="#{'foo"bar'}") bar})) #'
-    assert_equal(%Q{<a a="foo'bar">bar</a>\n}, render(%q{%a(a="#{"foo'bar"}") bar})) #'
+    assert_equal(%Q{<a a='foo&#039;bar'>bar</a>\n}, render(%q{%a(a="#{"foo'bar"}") bar})) #'
     assert_equal(%Q{<a a='foo"bar'>bar</a>\n}, render(%q{%a(a='foo"bar') bar}))
-    assert_equal(%Q{<a a="foo'bar">bar</a>\n}, render(%q{%a(a="foo'bar") bar}))
+    assert_equal(%Q{<a a='foo&#039;bar'>bar</a>\n}, render(%q{%a(a="foo'bar") bar}))
     assert_equal("<a a:b='foo'>bar</a>\n", render("%a(a:b='foo') bar"))
     assert_equal("<a a='foo' b='bar'>bar</a>\n", render("%a(a = 'foo' b = 'bar') bar"))
     assert_equal("<a a='foo' b='bar'>bar</a>\n", render("%a(a = foo b = bar) bar", :locals => {:foo => 'foo', :bar => 'bar'}))
     assert_equal("<a a='foo'>(b='bar')</a>\n", render("%a(a='foo')(b='bar')"))
     assert_equal("<a a='foo)bar'>baz</a>\n", render("%a(a='foo)bar') baz"))
@@ -1711,12 +1711,12 @@ HAML
 
   def test_new_attribute_escaping
     assert_equal(%Q{<a a='foo " bar'>bar</a>\n}, render(%q{%a(a="foo \" bar") bar}))
     assert_equal(%Q{<a a='foo \\" bar'>bar</a>\n}, render(%q{%a(a="foo \\\\\" bar") bar}))
 
-    assert_equal(%Q{<a a="foo ' bar">bar</a>\n}, render(%q{%a(a='foo \' bar') bar}))
-    assert_equal(%Q{<a a="foo \\' bar">bar</a>\n}, render(%q{%a(a='foo \\\\\' bar') bar}))
+    assert_equal(%Q{<a a='foo &#039; bar'>bar</a>\n}, render(%q{%a(a='foo \' bar') bar}))
+    assert_equal(%Q{<a a='foo \\&#039; bar'>bar</a>\n}, render(%q{%a(a='foo \\\\\' bar') bar}))
 
     assert_equal(%Q{<a a='foo \\ bar'>bar</a>\n}, render(%q{%a(a="foo \\\\ bar") bar}))
     assert_equal(%Q{<a a='foo \#{1 + 1} bar'>bar</a>\n}, render(%q{%a(a="foo \#{1 + 1} bar") bar}))
   end
 
diff --git a/test/pretty_engine_test.rb b/test/pretty_engine_test.rb
index 9e8963db..6c1a77a3 100644
--- a/test/pretty_engine_test.rb
+++ b/test/pretty_engine_test.rb
@@ -1127,12 +1127,12 @@ HAML
   end
 
   def test_attr_wrapper
     assert_equal("<p strange=*attrs*></p>\n", render("%p{ :strange => 'attrs'}", :attr_wrapper => '*'))
     assert_equal("<p escaped='quo\"te'></p>\n", render("%p{ :escaped => 'quo\"te'}", :attr_wrapper => '"'))
-    assert_equal("<p escaped=\"quo'te\"></p>\n", render("%p{ :escaped => 'quo\\'te'}", :attr_wrapper => '"'))
-    assert_equal("<p escaped=\"q'uo&#x0022;te\"></p>\n", render("%p{ :escaped => 'q\\'uo\"te'}", :attr_wrapper => '"'))
+    assert_equal("<p escaped=\"quo&#039;te\"></p>\n", render("%p{ :escaped => 'quo\\'te'}", :attr_wrapper => '"'))
+    assert_equal("<p escaped='q&#039;uo\"te'></p>\n", render("%p{ :escaped => 'q\\'uo\"te'}", :attr_wrapper => '"'))
     assert_equal("<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n", render("!!! XML", :attr_wrapper => '"', :format => :xhtml))
   end
 
   def test_autoclose_option
     assert_equal("<flaz foo='bar'>\n", render("%flaz{:foo => 'bar'}", :autoclose => ["flaz"]))
@@ -1525,11 +1525,11 @@ HAML
 
     assert_equal("<div data-one_plus_one='2'></div>\n",
       render("%div{:data => {:one_plus_one => 1+1}}",
         :hyphenate_data_attrs => false))
 
-    assert_equal("<div data-foo='Here&#x0027;s a \"quoteful\" string.'></div>\n",
+    assert_equal("<div data-foo='Here&#039;s a \"quoteful\" string.'></div>\n",
       render(%{%div{:data => {:foo => %{Here's a "quoteful" string.}}}},
         :hyphenate_data_attrs => false)) #'
   end
 
   def test_html5_data_attributes_with_hyphens
@@ -1689,13 +1689,13 @@ HAML
   end
 
   def test_new_attribute_parsing
     assert_equal("<a a2='b2'>bar</a>\n", render("%a(a2=b2) bar", :locals => {:b2 => 'b2'}))
     assert_equal(%Q{<a a='foo"bar'>bar</a>\n}, render(%q{%a(a="#{'foo"bar'}") bar})) #'
-    assert_equal(%Q{<a a="foo'bar">bar</a>\n}, render(%q{%a(a="#{"foo'bar"}") bar})) #'
+    assert_equal(%Q{<a a='foo&#039;bar'>bar</a>\n}, render(%q{%a(a="#{"foo'bar"}") bar})) #'
     assert_equal(%Q{<a a='foo"bar'>bar</a>\n}, render(%q{%a(a='foo"bar') bar}))
-    assert_equal(%Q{<a a="foo'bar">bar</a>\n}, render(%q{%a(a="foo'bar") bar}))
+    assert_equal(%Q{<a a='foo&#039;bar'>bar</a>\n}, render(%q{%a(a="foo'bar") bar}))
     assert_equal("<a a:b='foo'>bar</a>\n", render("%a(a:b='foo') bar"))
     assert_equal("<a a='foo' b='bar'>bar</a>\n", render("%a(a = 'foo' b = 'bar') bar"))
     assert_equal("<a a='foo' b='bar'>bar</a>\n", render("%a(a = foo b = bar) bar", :locals => {:foo => 'foo', :bar => 'bar'}))
     assert_equal("<a a='foo'>(b='bar')</a>\n", render("%a(a='foo')(b='bar')"))
     assert_equal("<a a='foo)bar'>baz</a>\n", render("%a(a='foo)bar') baz"))
@@ -1704,12 +1704,12 @@ HAML
 
   def test_new_attribute_escaping
     assert_equal(%Q{<a a='foo " bar'>bar</a>\n}, render(%q{%a(a="foo \" bar") bar}))
     assert_equal(%Q{<a a='foo \\" bar'>bar</a>\n}, render(%q{%a(a="foo \\\\\" bar") bar}))
 
-    assert_equal(%Q{<a a="foo ' bar">bar</a>\n}, render(%q{%a(a='foo \' bar') bar}))
-    assert_equal(%Q{<a a="foo \\' bar">bar</a>\n}, render(%q{%a(a='foo \\\\\' bar') bar}))
+    assert_equal(%Q{<a a='foo &#039; bar'>bar</a>\n}, render(%q{%a(a='foo \' bar') bar}))
+    assert_equal(%Q{<a a='foo \\&#039; bar'>bar</a>\n}, render(%q{%a(a='foo \\\\\' bar') bar}))
 
     assert_equal(%Q{<a a='foo \\ bar'>bar</a>\n}, render(%q{%a(a="foo \\\\ bar") bar}))
     assert_equal(%Q{<a a='foo \#{1 + 1} bar'>bar</a>\n}, render(%q{%a(a="foo \#{1 + 1} bar") bar}))
   end
 
diff --git a/test/pretty_results/just_stuff.xhtml b/test/pretty_results/just_stuff.xhtml
index 0df460f8..d7d166d3 100644
--- a/test/pretty_results/just_stuff.xhtml
+++ b/test/pretty_results/just_stuff.xhtml
@@ -4,11 +4,11 @@
 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">
-<strong apos="Foo's bar!">Boo!</strong>
+<strong apos='Foo&#039;s bar!'>Boo!</strong>
 Embedded? false!
 Embedded? true!
 Embedded? true!
 Embedded? twice! true!
 Embedded? one af"t"er another!
@@ -59,11 +59,11 @@ testtest
 <p class='article foo' id='article_1'>Blah</p>
 <p class='article bar baz foo' id='article_1'>Blah</p>
 <p class='article quux qux' id='article_1'>Blump</p>
 <p class='article' id='foo_bar_baz_article_1'>Whee</p>
 Woah inner quotes
-<p class='dynamic_quote' dyn='3' quotes="single '"></p>
+<p class='dynamic_quote' dyn='3' quotes='single &#039;'></p>
 <p class='dynamic_self_closing' dyn='3' />
 <body>
   hello
   <div>
     <img />
diff --git a/test/results/just_stuff.xhtml b/test/results/just_stuff.xhtml
index d31dd78b..742040d2 100644
--- a/test/results/just_stuff.xhtml
+++ b/test/results/just_stuff.xhtml
@@ -4,11 +4,11 @@
 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">
-<strong apos="Foo's bar!">Boo!</strong>
+<strong apos='Foo&#039;s bar!'>Boo!</strong>
 Embedded? false!
 Embedded? true!
 Embedded? true!
 Embedded? twice! true!
 Embedded? one af"t"er another!
@@ -59,11 +59,11 @@ Nested content
 <p class='article foo' id='article_1'>Blah</p>
 <p class='article bar baz foo' id='article_1'>Blah</p>
 <p class='article quux qux' id='article_1'>Blump</p>
 <p class='article' id='foo_bar_baz_article_1'>Whee</p>
 Woah inner quotes
-<p class='dynamic_quote' dyn='3' quotes="single '"></p>
+<p class='dynamic_quote' dyn='3' quotes='single &#039;'></p>
 <p class='dynamic_self_closing' dyn='3' />
 <body>
 hello
 <div>
 <img />

Vulnerable code:     HTML_ESCAPE_REGEX = /[\"><&]/
    assert_equal("<p escaped=\"quo'te\"></p>\n", render("%p{ :escaped => 'quo\\'te'}", :attr_wrapper => '"'))
    assert_equal("<p escaped=\"q'uo&#x0022;te\"></p>\n", render("%p{ :escaped => 'q\\'uo\"te'}", :attr_wrapper => '"'))
    assert_equal("<div data-foo='Here&#x0027;s a \"quoteful\" string.'></div>\n",
    assert_equal(%Q{<a a="foo'bar">bar</a>\n}, render(%q{%a(a="#{"foo'bar"}") bar})) #'
    assert_equal(%Q{<a a="foo'bar">bar</a>\n}, render(%q{%a(a="foo'bar") bar}))
    assert_equal(%Q{<a a="foo ' bar">bar</a>\n}, render(%q{%a(a='foo \' bar') bar}))
    assert_equal(%Q{<a a="foo \\' bar">bar</a>\n}, render(%q{%a(a='foo \\\\\' bar') bar}))
    assert_equal("<p escaped=\"quo'te\"></p>\n", render("%p{ :escaped => 'quo\\'te'}", :attr_wrapper => '"'))
    assert_equal("<p escaped=\"q'uo&#x0022;te\"></p>\n", render("%p{ :escaped => 'q\\'uo\"te'}", :attr_wrapper => '"'))
    assert_equal("<div data-foo='Here&#x0027;s a \"quoteful\" string.'></div>\n",
    assert_equal(%Q{<a a="foo'bar">bar</a>\n}, render(%q{%a(a="#{"foo'bar"}") bar})) #'
    assert_equal(%Q{<a a="foo'bar">bar</a>\n}, render(%q{%a(a="foo'bar") bar}))
    assert_equal(%Q{<a a="foo ' bar">bar</a>\n}, render(%q{%a(a='foo \' bar') bar}))
    assert_equal(%Q{<a a="foo \\' bar">bar</a>\n}, render(%q{%a(a='foo \\\\\' bar') bar}))
<strong apos="Foo's bar!">Boo!</strong>
<p class='dynamic_quote' dyn='3' quotes="single '"></p>
<strong apos="Foo's bar!">Boo!</strong>
<p class='dynamic_quote' dyn='3' quotes="single '"></p>
Fixed code:     HTML_ESCAPE_REGEX = /['"><&]/
    assert_equal("<p escaped=\"quo&#039;te\"></p>\n", render("%p{ :escaped => 'quo\\'te'}", :attr_wrapper => '"'))
    assert_equal("<p escaped='q&#039;uo\"te'></p>\n", render("%p{ :escaped => 'q\\'uo\"te'}", :attr_wrapper => '"'))
    assert_equal("<div data-foo='Here&#039;s a \"quoteful\" string.'></div>\n",
    assert_equal(%Q{<a a='foo&#039;bar'>bar</a>\n}, render(%q{%a(a="#{"foo'bar"}") bar})) #'
    assert_equal(%Q{<a a='foo&#039;bar'>bar</a>\n}, render(%q{%a(a="foo'bar") bar}))
    assert_equal(%Q{<a a='foo &#039; bar'>bar</a>\n}, render(%q{%a(a='foo \' bar') bar}))
    assert_equal(%Q{<a a='foo \\&#039; bar'>bar</a>\n}, render(%q{%a(a='foo \\\\\' bar') bar}))
    assert_equal("<p escaped=\"quo&#039;te\"></p>\n", render("%p{ :escaped => 'quo\\'te'}", :attr_wrapper => '"'))
    assert_equal("<p escaped='q&#039;uo\"te'></p>\n", render("%p{ :escaped => 'q\\'uo\"te'}", :attr_wrapper => '"'))
    assert_equal("<div data-foo='Here&#039;s a \"quoteful\" string.'></div>\n",
    assert_equal(%Q{<a a='foo&#039;bar'>bar</a>\n}, render(%q{%a(a="#{"foo'bar"}") bar})) #'
    assert_equal(%Q{<a a='foo&#039;bar'>bar</a>\n}, render(%q{%a(a="foo'bar") bar}))
    assert_equal(%Q{<a a='foo &#039; bar'>bar</a>\n}, render(%q{%a(a='foo \' bar') bar}))
    assert_equal(%Q{<a a='foo \\&#039; bar'>bar</a>\n}, render(%q{%a(a='foo \\\\\' bar') bar}))
<strong apos='Foo&#039;s bar!'>Boo!</strong>
<p class='dynamic_quote' dyn='3' quotes='single &#039;'></p>
<strong apos='Foo&#039;s bar!'>Boo!</strong>
<p class='dynamic_quote' dyn='3' quotes='single &#039;'></p>