From c94e24fbe7bcdf605cafcfabdf97454d1e1e0685 Mon Sep 17 00:00:00 2001 From: Timm Date: Wed, 12 Jun 2013 15:59:34 +0200 Subject: Added Loofah as a dependency in actionview.gemspec. Implemented ActionView: FullSanitizer, LinkSanitizer and WhiteListSanitizer in sanitizers.rb. Deprecated protocol_separator and bad_tags. Added new tests in sanitizers_test.rb and reimplemented assert_dom_equal with Loofah. --- Gemfile | 3 + actionpack/actionpack.gemspec | 2 + .../lib/action_dispatch/testing/assertions/dom.rb | 18 +- actionview/actionview.gemspec | 1 + .../lib/action_view/helpers/sanitize_helper.rb | 47 +-- .../helpers/sanitize_helper/sanitizers.rb | 116 ++++++++ actionview/test/template/sanitizers_test.rb | 330 +++++++++++++++++++++ 7 files changed, 485 insertions(+), 32 deletions(-) create mode 100644 actionview/lib/action_view/helpers/sanitize_helper/sanitizers.rb create mode 100644 actionview/test/template/sanitizers_test.rb diff --git a/Gemfile b/Gemfile index 2a695df618..1cc3a4d07f 100644 --- a/Gemfile +++ b/Gemfile @@ -2,6 +2,9 @@ source 'https://rubygems.org' gemspec +# temporary gem while working on loofah integration +gem 'loofah', '~> 1.2.1', github: 'kaspth/loofah' + # This needs to be with require false as it is # loaded after loading the test library to # ensure correct loading order diff --git a/actionpack/actionpack.gemspec b/actionpack/actionpack.gemspec index 1d6009bab8..47a3d96cc8 100644 --- a/actionpack/actionpack.gemspec +++ b/actionpack/actionpack.gemspec @@ -21,6 +21,8 @@ Gem::Specification.new do |s| s.add_dependency 'activesupport', version + s.add_dependency 'loofah', '~> 1.2.1' + s.add_dependency 'rack', '~> 1.5.2' s.add_dependency 'rack-test', '~> 0.6.2' s.add_dependency 'actionview', version diff --git a/actionpack/lib/action_dispatch/testing/assertions/dom.rb b/actionpack/lib/action_dispatch/testing/assertions/dom.rb index 241a39393a..d929e4b400 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/dom.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/dom.rb @@ -1,4 +1,4 @@ -require 'action_view/vendor/html-scanner' +require 'loofah' module ActionDispatch module Assertions @@ -7,20 +7,20 @@ module ActionDispatch # # # assert that the referenced method generates the appropriate HTML string # assert_dom_equal 'Apples', link_to("Apples", "http://www.example.com") - def assert_dom_equal(expected, actual, message = nil) - expected_dom = HTML::Document.new(expected).root - actual_dom = HTML::Document.new(actual).root - assert_equal expected_dom, actual_dom, message + def assert_dom_equal(expected, actual, message = "") + expected_dom = Loofah.fragment(expected).to_s + actual_dom = Loofah.fragment(actual).to_s + assert_equal expected_dom, actual_dom end # The negated form of +assert_dom_equivalent+. # # # assert that the referenced method does not generate the specified HTML string # assert_dom_not_equal 'Apples', link_to("Oranges", "http://www.example.com") - def assert_dom_not_equal(expected, actual, message = nil) - expected_dom = HTML::Document.new(expected).root - actual_dom = HTML::Document.new(actual).root - assert_not_equal expected_dom, actual_dom, message + def assert_dom_not_equal(expected, actual, message = "") + expected_dom = Loofah.fragment(expected).to_s + actual_dom = Loofah.fragment(actual).to_s + assert_not_equal expected_dom, actual_dom end end end diff --git a/actionview/actionview.gemspec b/actionview/actionview.gemspec index e45dd04225..578e5968e4 100644 --- a/actionview/actionview.gemspec +++ b/actionview/actionview.gemspec @@ -23,6 +23,7 @@ Gem::Specification.new do |s| s.add_dependency 'builder', '~> 3.1' s.add_dependency 'erubis', '~> 2.7.0' + s.add_dependency 'loofah', '~> 1.2.1' s.add_development_dependency 'actionpack', version s.add_development_dependency 'activemodel', version diff --git a/actionview/lib/action_view/helpers/sanitize_helper.rb b/actionview/lib/action_view/helpers/sanitize_helper.rb index 049af275b6..66894b5936 100644 --- a/actionview/lib/action_view/helpers/sanitize_helper.rb +++ b/actionview/lib/action_view/helpers/sanitize_helper.rb @@ -1,5 +1,5 @@ require 'active_support/core_ext/object/try' -require 'action_view/vendor/html-scanner' +require 'action_view/helpers/sanitize_helper/sanitizers' module ActionView # = Action View Sanitize Helpers @@ -65,9 +65,9 @@ module ActionView self.class.white_list_sanitizer.sanitize_css(style) end - # Strips all HTML tags from the +html+, including comments. This uses the - # html-scanner tokenizer and so its HTML parsing ability is limited by - # that of html-scanner. + # Strips all HTML tags from the +html+, including comments. This uses + # Nokogiri for tokenization (via Loofah) and so its HTML parsing ability + # is limited by that of Nokogiri. # # strip_tags("Strip these tags!") # # => Strip these tags! @@ -134,11 +134,7 @@ module ActionView white_list_sanitizer.allowed_protocols end - def sanitized_protocol_separator=(value) - white_list_sanitizer.protocol_separator = value - end - - # Gets the HTML::FullSanitizer instance used by +strip_tags+. Replace with + # Gets the ActionView::FullSanitizer instance used by +strip_tags+. Replace with # any object that responds to +sanitize+. # # class Application < Rails::Application @@ -146,21 +142,21 @@ module ActionView # end # def full_sanitizer - @full_sanitizer ||= HTML::FullSanitizer.new + @full_sanitizer ||= ActionView::FullSanitizer.new end - # Gets the HTML::LinkSanitizer instance used by +strip_links+. Replace with - # any object that responds to +sanitize+. + # Gets the ActionView::LinkSanitizer instance used by +strip_links+. + # Replace with any object that responds to +sanitize+. # # class Application < Rails::Application # config.action_view.link_sanitizer = MySpecialSanitizer.new # end # def link_sanitizer - @link_sanitizer ||= HTML::LinkSanitizer.new + @link_sanitizer ||= ActionView::LinkSanitizer.new end - # Gets the HTML::WhiteListSanitizer instance used by sanitize and +sanitize_css+. + # Gets the ActionView::WhiteListSanitizer instance used by sanitize and +sanitize_css+. # Replace with any object that responds to +sanitize+. # # class Application < Rails::Application @@ -168,7 +164,12 @@ module ActionView # end # def white_list_sanitizer - @white_list_sanitizer ||= HTML::WhiteListSanitizer.new + @white_list_sanitizer ||= ActionView::WhiteListSanitizer.new + end + + + def sanitized_protocol_separator=(value) + ActionView::WhiteListSanitizer.protocol_separator = value end # Adds valid HTML attributes that the +sanitize+ helper checks for URIs. @@ -178,7 +179,7 @@ module ActionView # end # def sanitized_uri_attributes=(attributes) - HTML::WhiteListSanitizer.uri_attributes.merge(attributes) + ActionView::WhiteListSanitizer.update_uri_attributes(attributes) end # Adds to the Set of 'bad' tags for the +sanitize+ helper. @@ -188,7 +189,7 @@ module ActionView # end # def sanitized_bad_tags=(attributes) - HTML::WhiteListSanitizer.bad_tags.merge(attributes) + ActionView::WhiteListSanitizer.bad_tags = attributes end # Adds to the Set of allowed tags for the +sanitize+ helper. @@ -198,7 +199,7 @@ module ActionView # end # def sanitized_allowed_tags=(attributes) - HTML::WhiteListSanitizer.allowed_tags.merge(attributes) + ActionView::WhiteListSanitizer.update_allowed_tags(attributes) end # Adds to the Set of allowed HTML attributes for the +sanitize+ helper. @@ -208,7 +209,7 @@ module ActionView # end # def sanitized_allowed_attributes=(attributes) - HTML::WhiteListSanitizer.allowed_attributes.merge(attributes) + ActionView::WhiteListSanitizer.update_allowed_attributes(attributes) end # Adds to the Set of allowed CSS properties for the #sanitize and +sanitize_css+ helpers. @@ -218,7 +219,7 @@ module ActionView # end # def sanitized_allowed_css_properties=(attributes) - HTML::WhiteListSanitizer.allowed_css_properties.merge(attributes) + ActionView::WhiteListSanitizer.update_allowed_css_properties(attributes) end # Adds to the Set of allowed CSS keywords for the +sanitize+ and +sanitize_css+ helpers. @@ -228,7 +229,7 @@ module ActionView # end # def sanitized_allowed_css_keywords=(attributes) - HTML::WhiteListSanitizer.allowed_css_keywords.merge(attributes) + ActionView::WhiteListSanitizer.update_allowed_css_keywords(attributes) end # Adds to the Set of allowed shorthand CSS properties for the +sanitize+ and +sanitize_css+ helpers. @@ -238,7 +239,7 @@ module ActionView # end # def sanitized_shorthand_css_properties=(attributes) - HTML::WhiteListSanitizer.shorthand_css_properties.merge(attributes) + ActionView::WhiteListSanitizer.update_shorthand_css_properties(attributes) end # Adds to the Set of allowed protocols for the +sanitize+ helper. @@ -248,7 +249,7 @@ module ActionView # end # def sanitized_allowed_protocols=(attributes) - HTML::WhiteListSanitizer.allowed_protocols.merge(attributes) + ActionView::WhiteListSanitizer.update_allowed_protocols(attributes) end end end diff --git a/actionview/lib/action_view/helpers/sanitize_helper/sanitizers.rb b/actionview/lib/action_view/helpers/sanitize_helper/sanitizers.rb new file mode 100644 index 0000000000..74be525581 --- /dev/null +++ b/actionview/lib/action_view/helpers/sanitize_helper/sanitizers.rb @@ -0,0 +1,116 @@ +require 'active_support/core_ext/class/attribute' +require 'active_support/deprecation' +require 'loofah' + +module ActionView + + class FullSanitizer + def sanitize(html, options = {}) + Loofah.fragment(html).text + end + end + + class LinkSanitizer + def initialize + @link_scrubber = Loofah::Scrubber.new do |node| + next unless node.name == 'a' + node.before node.children + node.remove + end + end + + def sanitize(html, options = {}) + Loofah.scrub_fragment(html, @link_scrubber).to_s + end + end + + class WhiteListSanitizer + def sanitize(html, options = {}) + return nil unless html + validate_options(options) + + loofah_fragment = Loofah.fragment(html) + loofah_fragment.scrub!(:strip) + loofah_fragment.xpath("./form").each { |form| form.remove } + loofah_fragment.to_s + end + + def sanitize_css(style_string) + Loofah::HTML5::Scrub.scrub_css style_string + end + + def protocol_separator + ActiveSupport::Deprecation.warn('protocol_separator has been deprecated and has no effect.') + end + + def protocol_separator=(value) + ActiveSupport::Deprecation.warn('protocol_separator= has been deprecated and has no effect.') + end + + def bad_tags + ActiveSupport::Deprecation.warn('bad_tags has been deprecated and has no effect.') + end + + class << self + def protocol_separator + ActiveSupport::Deprecation.warn('protocol_separator has been deprecated and has no effect.') + end + + def protocol_separator=(value) + ActiveSupport::Deprecation.warn('protocol_separator= has been deprecated and has no effect.') + end + + def bad_tags + ActiveSupport::Deprecation.warn('The bad_tags class attribute has been deprecated and has no effect. You can still affect the tags being sanitized using bad_tags= which changes the allowed_tags.') + end + + def bad_tags=(tags) + allowed_tags.replace(allowed_tags - tags) + end + end + + [:uri_attributes, :allowed_attributes, + :allowed_tags, :allowed_protocols, :allowed_css_properties, + :allowed_css_keywords, :shorthand_css_properties].each do |attr| + class_attribute attr, :instance_writer => false + + define_method "#{self}.update_#{attr}" do |arg| + attr.merge arg + end + end + + # Constants are from Loofahs source at lib/loofah/html5/whitelist.rb + self.uri_attributes = Loofah::HTML5::WhiteList::ATTR_VAL_IS_URI + + self.allowed_tags = Loofah::HTML5::WhiteList::ALLOWED_ELEMENTS + + self.bad_tags = Set.new %w(script) + + self.allowed_attributes = Loofah::HTML5::WhiteList::ALLOWED_ATTRIBUTES + + self.allowed_css_properties = Loofah::HTML5::WhiteList::ALLOWED_CSS_PROPERTIES + + self.allowed_css_keywords = Loofah::HTML5::WhiteList::ALLOWED_CSS_KEYWORDS + + self.shorthand_css_properties = Loofah::HTML5::WhiteList::SHORTHAND_CSS_PROPERTIES + + self.allowed_protocols = Loofah::HTML5::WhiteList::ALLOWED_PROTOCOLS + + protected + def validate_options(options) + if options[:tags] && !options[:tags].is_a?(Enumerable) + raise ArgumentError, "You should pass :tags as an Enumerable" + end + + if options[:attributes] && !options[:attributes].is_a?(Enumerable) + raise ArgumentError, "You should pass :attributes as an Enumerable" + end + end + + def contains_bad_protocols?(attr_name, value) + protocol_separator = ':' + self.uri_attributes.include?(attr_name) && + (value =~ /(^[^\/:]*):|(�*58)|(p)|(�*3a)|(%|%)3A/i && !self.allowed_protocols.include?(value.split(protocol_separator).first.downcase.strip)) + end + end +end diff --git a/actionview/test/template/sanitizers_test.rb b/actionview/test/template/sanitizers_test.rb new file mode 100644 index 0000000000..dc2fcf61e8 --- /dev/null +++ b/actionview/test/template/sanitizers_test.rb @@ -0,0 +1,330 @@ +require 'abstract_unit' + +class SanitizerTest < ActionController::TestCase + def setup + @sanitizer = nil # used by assert_sanitizer + end + + def test_strip_tags_with_quote + sanitizer = ActionView::FullSanitizer.new + string = '<" hi' + + assert_equal ' hi', sanitizer.sanitize(string) + end + + def test_strip_tags + sanitizer = ActionView::FullSanitizer.new + assert_equal("<<")) + assert_equal("Dont touch me", sanitizer.sanitize("Dont touch me")) + assert_equal("This is a test.", sanitizer.sanitize("

This is a test.

")) + assert_equal("Weirdos", sanitizer.sanitize("Wei<a onclick='alert(document.cookie);'/>rdos")) + assert_equal("This is a test.", sanitizer.sanitize("This is a test.")) + assert_equal( + %{This is a test.\n\n\nIt no longer contains any HTML.\n}, sanitizer.sanitize( + %{This is <b>a <a href="" target="_blank">test</a></b>.\n\n\n\n

It no longer contains any HTML.

\n})) + assert_equal "This has a here.", sanitizer.sanitize("This has a here.") + assert_equal "This has a here.", sanitizer.sanitize("This has a ]]> here.") + assert_equal "This has an unclosed ", sanitizer.sanitize("This has an unclosed ]] here...") + [nil, '', ' '].each { |blank| assert_equal blank, sanitizer.sanitize(blank) } + assert_nothing_raised { sanitizer.sanitize("This is a frozen string with no tags".freeze) } + end + + def test_strip_links + sanitizer = ActionView::LinkSanitizer.new + assert_equal "Dont touch me", sanitizer.sanitize("Dont touch me") + assert_equal "on my mind\nall day long", sanitizer.sanitize("on my mind\nall day long") + assert_equal "0wn3d", sanitizer.sanitize("0wn3d") + assert_equal "Magic", sanitizer.sanitize("Magic") + assert_equal "FrrFox", sanitizer.sanitize("FrrFox") + assert_equal "My mind\nall day long", sanitizer.sanitize("My mind\nall day long") + assert_equal "all day long", sanitizer.sanitize("<a href='hello'>all day long</a>") + + assert_equal "", '' + end + + def test_sanitize_plaintext + raw = "<span>foo</span></plaintext>" + assert_sanitized raw, "<span>foo</span>" + end + + def test_sanitize_script + assert_sanitized "a b c<script language=\"Javascript\">blah blah blah</script>d e f", "a b cblah blah blahd e f" + end + + def test_sanitize_js_handlers + raw = %{onthis="do that" <a href="#" onclick="hello" name="foo" onbogus="remove me">hello</a>} + assert_sanitized raw, %{onthis="do that" <a href="#" name="foo">hello</a>} + end + + def test_sanitize_javascript_href + raw = %{href="javascript:bang" <a href="javascript:bang" name="hello">foo</a>, <span href="javascript:bang">bar</span>} + assert_sanitized raw, %{href="javascript:bang" <a name="hello">foo</a>, <span>bar</span>} + end + + def test_sanitize_image_src + raw = %{src="javascript:bang" <img src="javascript:bang" width="5">foo</img>, <span src="javascript:bang">bar</span>} + assert_sanitized raw, %{src="javascript:bang" <img width="5">foo</img>, <span>bar</span>} + end + + ActionView::WhiteListSanitizer.allowed_tags.each do |tag_name| + define_method "test_should_allow_#{tag_name}_tag" do + assert_sanitized "start <#{tag_name} title=\"1\" onclick=\"foo\">foo <bad>bar</bad> baz</#{tag_name}> end", %(start <#{tag_name} title="1">foo bar baz</#{tag_name}> end) + end + end + + def test_should_allow_anchors + assert_sanitized %(<a href="foo" onclick="bar"><script>baz</script></a>), %(<a href=\"foo\">baz</a>) + end + + # RFC 3986, sec 4.2 + def test_allow_colons_in_path_component + assert_sanitized("<a href=\"./this:that\">foo</a>") + end + + %w(src width height alt).each do |img_attr| + define_method "test_should_allow_image_#{img_attr}_attribute" do + assert_sanitized %(<img #{img_attr}="foo" onclick="bar" />), %(<img #{img_attr}="foo" />) + end + end + + def test_should_handle_non_html + assert_sanitized 'abc' + end + + def test_should_handle_blank_text + assert_sanitized nil + assert_sanitized '' + end + + def test_should_allow_custom_tags + text = "<u>foo</u>" + sanitizer = ActionView::WhiteListSanitizer.new + assert_equal(text, sanitizer.sanitize(text, :tags => %w(u))) + end + + def test_should_allow_only_custom_tags + text = "<u>foo</u> with <i>bar</i>" + sanitizer = ActionView::WhiteListSanitizer.new + assert_equal("<u>foo</u> with bar", sanitizer.sanitize(text, :tags => %w(u))) + end + + def test_should_allow_custom_tags_with_attributes + text = %(<blockquote cite="http://example.com/">foo</blockquote>) + sanitizer = ActionView::WhiteListSanitizer.new + assert_equal(text, sanitizer.sanitize(text)) + end + + def test_should_allow_custom_tags_with_custom_attributes + text = %(<blockquote foo="bar">Lorem ipsum</blockquote>) + sanitizer = ActionView::WhiteListSanitizer.new + assert_equal(text, sanitizer.sanitize(text, :attributes => ['foo'])) + end + + def test_should_raise_argument_error_if_tags_is_not_enumerable + sanitizer = ActionView::WhiteListSanitizer.new + e = assert_raise(ArgumentError) do + sanitizer.sanitize('', :tags => 'foo') + end + + assert_equal "You should pass :tags as an Enumerable", e.message + end + + def test_should_raise_argument_error_if_attributes_is_not_enumerable + sanitizer = ActionView::WhiteListSanitizer.new + e = assert_raise(ArgumentError) do + sanitizer.sanitize('', :attributes => 'foo') + end + + assert_equal "You should pass :attributes as an Enumerable", e.message + end + + [%w(img src), %w(a href)].each do |(tag, attr)| + define_method "test_should_strip_#{attr}_attribute_in_#{tag}_with_bad_protocols" do + assert_sanitized %(<#{tag} #{attr}="javascript:bang" title="1">boo</#{tag}>), %(<#{tag} title="1">boo</#{tag}>) + end + end + + def test_should_flag_bad_protocols + sanitizer = ActionView::WhiteListSanitizer.new + %w(about chrome data disk hcp help javascript livescript lynxcgi lynxexec ms-help ms-its mhtml mocha opera res resource shell vbscript view-source vnd.ms.radio wysiwyg).each do |proto| + assert sanitizer.send(:contains_bad_protocols?, 'src', "#{proto}://bad") + end + end + + def test_should_accept_good_protocols_ignoring_case + sanitizer = ActionView::WhiteListSanitizer.new + ActionView::WhiteListSanitizer.allowed_protocols.each do |proto| + assert !sanitizer.send(:contains_bad_protocols?, 'src', "#{proto.capitalize}://good") + end + end + + def test_should_accept_good_protocols_ignoring_space + sanitizer = ActionView::WhiteListSanitizer.new + ActionView::WhiteListSanitizer.allowed_protocols.each do |proto| + assert !sanitizer.send(:contains_bad_protocols?, 'src', " #{proto}://good") + end + end + + def test_should_accept_good_protocols + sanitizer = ActionView::WhiteListSanitizer.new + ActionView::WhiteListSanitizer.allowed_protocols.each do |proto| + assert !sanitizer.send(:contains_bad_protocols?, 'src', "#{proto}://good") + end + end + + def test_should_reject_hex_codes_in_protocol + assert_sanitized %(<a href="&#37;6A&#37;61&#37;76&#37;61&#37;73&#37;63&#37;72&#37;69&#37;70&#37;74&#37;3A&#37;61&#37;6C&#37;65&#37;72&#37;74&#37;28&#37;22&#37;58&#37;53&#37;53&#37;22&#37;29">1</a>), "<a>1</a>" + assert @sanitizer.send(:contains_bad_protocols?, 'src', "%6A%61%76%61%73%63%72%69%70%74%3A%61%6C%65%72%74%28%22%58%53%53%22%29") + end + + def test_should_block_script_tag + assert_sanitized %(<SCRIPT\nSRC=http://ha.ckers.org/xss.js></SCRIPT>), "" + end + + [%(<IMG SRC="javascript:alert('XSS');">), + %(<IMG SRC=javascript:alert('XSS')>), + %(<IMG SRC=JaVaScRiPt:alert('XSS')>), + %(<IMG """><SCRIPT>alert("XSS")</SCRIPT>">), + %(<IMG SRC=javascript:alert(&quot;XSS&quot;)>), + %(<IMG SRC=javascript:alert(String.fromCharCode(88,83,83))>), + %(<IMG SRC=&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;&#97;&#108;&#101;&#114;&#116;&#40;&#39;&#88;&#83;&#83;&#39;&#41;>), + %(<IMG SRC=&#0000106&#0000097&#0000118&#0000097&#0000115&#0000099&#0000114&#0000105&#0000112&#0000116&#0000058&#0000097&#0000108&#0000101&#0000114&#0000116&#0000040&#0000039&#0000088&#0000083&#0000083&#0000039&#0000041>), + %(<IMG SRC=&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A&#x61&#x6C&#x65&#x72&#x74&#x28&#x27&#x58&#x53&#x53&#x27&#x29>), + %(<IMG SRC="jav\tascript:alert('XSS');">), + %(<IMG SRC="jav&#x09;ascript:alert('XSS');">), + %(<IMG SRC="jav&#x0A;ascript:alert('XSS');">), + %(<IMG SRC="jav&#x0D;ascript:alert('XSS');">), + %(<IMG SRC=" &#14; javascript:alert('XSS');">), + %(<IMG SRC="javascript&#x3a;alert('XSS');">), + %(<IMG SRC=`javascript:alert("RSnake says, 'XSS'")`>)].each_with_index do |img_hack, i| + define_method "test_should_not_fall_for_xss_image_hack_#{i+1}" do + assert_sanitized img_hack, "<img>" + end + end + + def test_should_sanitize_tag_broken_up_by_null + assert_sanitized %(<SCR\0IPT>alert(\"XSS\")</SCR\0IPT>), "alert(\"XSS\")" + end + + def test_should_sanitize_invalid_script_tag + assert_sanitized %(<SCRIPT/XSS SRC="http://ha.ckers.org/xss.js"></SCRIPT>), "" + end + + def test_should_sanitize_script_tag_with_multiple_open_brackets + assert_sanitized %(<<SCRIPT>alert("XSS");//<</SCRIPT>), "&lt;" + assert_sanitized %(<iframe src=http://ha.ckers.org/scriptlet.html\n<a), %(&lt;a) + end + + def test_should_sanitize_unclosed_script + assert_sanitized %(<SCRIPT SRC=http://ha.ckers.org/xss.js?<B>), "<b>" + end + + def test_should_sanitize_half_open_scripts + assert_sanitized %(<IMG SRC="javascript:alert('XSS')"), "<img>" + end + + def test_should_not_fall_for_ridiculous_hack + img_hack = %(<IMG\nSRC\n=\n"\nj\na\nv\na\ns\nc\nr\ni\np\nt\n:\na\nl\ne\nr\nt\n(\n'\nX\nS\nS\n'\n)\n"\n>) + assert_sanitized img_hack, "<img>" + end + + def test_should_sanitize_attributes + assert_sanitized %(<SPAN title="'><script>alert()</script>">blah</SPAN>), %(<span title="#{CGI.escapeHTML "'><script>alert()</script>"}">blah</span>) + end + + def test_should_sanitize_illegal_style_properties + raw = %(display:block; position:absolute; left:0; top:0; width:100%; height:100%; z-index:1; background-color:black; background-image:url(http://www.ragingplatypus.com/i/cam-full.jpg); background-x:center; background-y:center; background-repeat:repeat;) + expected = %(display: block; width: 100%; height: 100%; background-color: black; background-x: center; background-y: center;) + assert_equal expected, sanitize_css(raw) + end + + def test_should_sanitize_with_trailing_space + raw = "display:block; " + expected = "display: block;" + assert_equal expected, sanitize_css(raw) + end + + def test_should_sanitize_xul_style_attributes + raw = %(-moz-binding:url('http://ha.ckers.org/xssmoz.xml#xss')) + assert_equal '', sanitize_css(raw) + end + + def test_should_sanitize_invalid_tag_names + assert_sanitized(%(a b c<script/XSS src="http://ha.ckers.org/xss.js"></script>d e f), "a b cd e f") + end + + def test_should_sanitize_non_alpha_and_non_digit_characters_in_tags + assert_sanitized('<a onclick!#$%&()*~+-_.,:;?@[/|\]^`=alert("XSS")>foo</a>', "<a>foo</a>") + end + + def test_should_sanitize_invalid_tag_names_in_single_tags + assert_sanitized('<img/src="http://ha.ckers.org/xss.js"/>', "<img />") + end + + def test_should_sanitize_img_dynsrc_lowsrc + assert_sanitized(%(<img lowsrc="javascript:alert('XSS')" />), "<img />") + end + + def test_should_sanitize_div_background_image_unicode_encoded + raw = %(background-image:\0075\0072\006C\0028'\006a\0061\0076\0061\0073\0063\0072\0069\0070\0074\003a\0061\006c\0065\0072\0074\0028.1027\0058.1053\0053\0027\0029'\0029) + assert_equal '', sanitize_css(raw) + end + + def test_should_sanitize_div_style_expression + raw = %(width: expression(alert('XSS'));) + assert_equal '', sanitize_css(raw) + end + + def test_should_sanitize_across_newlines + raw = %(\nwidth:\nexpression(alert('XSS'));\n) + assert_equal '', sanitize_css(raw) + end + + def test_should_sanitize_img_vbscript + assert_sanitized %(<img src='vbscript:msgbox("XSS")' />), '<img />' + end + + def test_should_sanitize_cdata_section + assert_sanitized "<![CDATA[<span>section</span>]]>", "&lt;![CDATA[&lt;span>section&lt;/span>]]>" + end + + def test_should_sanitize_unterminated_cdata_section + assert_sanitized "<![CDATA[<span>neverending...", "&lt;![CDATA[&lt;span>neverending...]]>" + end + + def test_should_not_mangle_urls_with_ampersand + assert_sanitized %{<a href=\"http://www.domain.com?var1=1&amp;var2=2\">my link</a>} + end + + def test_should_sanitize_neverending_attribute + assert_sanitized "<span class=\"\\", "<span class=\"\\\">" + end + + def test_x03a + assert_sanitized %(<a href="javascript&#x3a;alert('XSS');">), "<a>" + assert_sanitized %(<a href="javascript&#x003a;alert('XSS');">), "<a>" + assert_sanitized %(<a href="http&#x3a;//legit">), %(<a href="http://legit">) + assert_sanitized %(<a href="javascript&#x3A;alert('XSS');">), "<a>" + assert_sanitized %(<a href="javascript&#x003A;alert('XSS');">), "<a>" + assert_sanitized %(<a href="http&#x3A;//legit">), %(<a href="http://legit">) + end + +protected + def assert_sanitized(input, expected = nil) + @sanitizer ||= ActionView::WhiteListSanitizer.new + if input + assert_dom_equal expected || input, @sanitizer.sanitize(input) + else + assert_nil @sanitizer.sanitize(input) + end + end + + def sanitize_css(input) + (@sanitizer ||= ActionView::WhiteListSanitizer.new).sanitize_css(input) + end +end -- cgit v1.2.3