Skip to content

Commit 297161e

Browse files
rafaelfrancatenderlove
authored andcommitted
Define a less permissive list of tags and attributes
And use it by default. The new sanitizer were being a lot more permissive that we had in Rails until the version 4.2. This was also allowing arbritary data attributes by default what can lead to CSRF and XSS attacks. Now data attributes need to be explicitly allowed. CVE-2015-7578
1 parent bc771d1 commit 297161e

File tree

3 files changed

+55
-10
lines changed

3 files changed

+55
-10
lines changed

Diff for: lib/rails/html/sanitizer.rb

+4
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,10 @@ class << self
9797
attr_accessor :allowed_tags
9898
attr_accessor :allowed_attributes
9999
end
100+
self.allowed_tags = Set.new(%w(strong em b i p code pre tt samp kbd var sub
101+
sup dfn cite big small address hr br div span h1 h2 h3 h4 h5 h6 ul ol li dl dt dd abbr
102+
acronym a img blockquote del ins))
103+
self.allowed_attributes = Set.new(%w(href src width height alt cite datetime title class name xml:lang abbr))
100104

101105
def initialize
102106
@permit_scrubber = PermitScrubber.new

Diff for: lib/rails/html/scrubbers.rb

+25
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ def scrub_attributes(node)
100100
if @attributes
101101
node.attribute_nodes.each do |attr|
102102
attr.remove if scrub_attribute?(attr.name)
103+
scrub_attribute(node, attr)
103104
end
104105

105106
scrub_css_attribute(node)
@@ -123,6 +124,30 @@ def validate!(var, name)
123124
end
124125
var
125126
end
127+
128+
def scrub_attribute(node, attr_node)
129+
attr_name = if attr_node.namespace
130+
"#{attr_node.namespace.prefix}:#{attr_node.node_name}"
131+
else
132+
attr_node.node_name
133+
end
134+
135+
if Loofah::HTML5::WhiteList::ATTR_VAL_IS_URI.include?(attr_name)
136+
# this block lifted nearly verbatim from HTML5 sanitization
137+
val_unescaped = CGI.unescapeHTML(attr_node.value).gsub(Loofah::HTML5::Scrub::CONTROL_CHARACTERS,'').downcase
138+
if val_unescaped =~ /^[a-z0-9][-+.a-z0-9]*:/ && ! Loofah::HTML5::WhiteList::ALLOWED_PROTOCOLS.include?(val_unescaped.split(Loofah::HTML5::WhiteList::PROTOCOL_SEPARATOR)[0])
139+
attr_node.remove
140+
end
141+
end
142+
if Loofah::HTML5::WhiteList::SVG_ATTR_VAL_ALLOWS_REF.include?(attr_name)
143+
attr_node.value = attr_node.value.gsub(/url\s*\(\s*[^#\s][^)]+?\)/m, ' ') if attr_node.value
144+
end
145+
if Loofah::HTML5::WhiteList::SVG_ALLOW_LOCAL_HREF.include?(node.name) && attr_name == 'xlink:href' && attr_node.value =~ /^\s*[^#\s].*/m
146+
attr_node.remove
147+
end
148+
149+
node.remove_attribute(attr_node.name) if attr_name == 'src' && attr_node.value !~ /[^[:space:]]/
150+
end
126151
end
127152

128153
# === Rails::Html::TargetScrubber

Diff for: test/sanitizer_test.rb

+26-10
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ def test_sanitize_plaintext
152152
end
153153

154154
def test_sanitize_script
155-
assert_sanitized "a b c<script language=\"Javascript\">blah blah blah</script>d e f", "a b cd e f"
155+
assert_sanitized "a b c<script language=\"Javascript\">blah blah blah</script>d e f", "a b cblah blah blahd e f"
156156
end
157157

158158
def test_sanitize_js_handlers
@@ -173,17 +173,23 @@ def test_sanitize_image_src
173173
tags = Loofah::HTML5::WhiteList::ALLOWED_ELEMENTS - %w(script form)
174174
tags.each do |tag_name|
175175
define_method "test_should_allow_#{tag_name}_tag" do
176-
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)
176+
scope_allowed_tags(tags) do
177+
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)
178+
end
177179
end
178180
end
179181

180182
def test_should_allow_anchors
181-
assert_sanitized %(<a href="foo" onclick="bar"><script>baz</script></a>), %(<a href=\"foo\"></a>)
183+
assert_sanitized %(<a href="foo" onclick="bar"><script>baz</script></a>), %(<a href=\"foo\">baz</a>)
182184
end
183185

184186
def test_video_poster_sanitization
185-
assert_sanitized %(<video src="videofile.ogg" autoplay poster="posterimage.jpg"></video>), %(<video src="videofile.ogg" poster="posterimage.jpg"></video>)
186-
assert_sanitized %(<video src="videofile.ogg" poster=javascript:alert(1)></video>), %(<video src="videofile.ogg"></video>)
187+
scope_allowed_tags(%w(video)) do
188+
scope_allowed_attributes %w(src poster) do
189+
assert_sanitized %(<video src="videofile.ogg" autoplay poster="posterimage.jpg"></video>), %(<video src="videofile.ogg" poster="posterimage.jpg"></video>)
190+
assert_sanitized %(<video src="videofile.ogg" poster=javascript:alert(1)></video>), %(<video src="videofile.ogg"></video>)
191+
end
192+
end
187193
end
188194

189195
# RFC 3986, sec 4.2
@@ -309,7 +315,7 @@ def test_should_block_script_tag
309315
end
310316

311317
def test_should_not_fall_for_xss_image_hack_with_uppercase_tags
312-
assert_sanitized %(<IMG """><SCRIPT>alert("XSS")</SCRIPT>">), "<img>\"&gt;"
318+
assert_sanitized %(<IMG """><SCRIPT>alert("XSS")</SCRIPT>">), %(<img>alert("XSS")"&gt;)
313319
end
314320

315321
[%(<IMG SRC="javascript:alert('XSS');">),
@@ -453,6 +459,16 @@ def test_sanitize_ascii_8bit_string
453459
end
454460
end
455461

462+
def test_sanitize_data_attributes
463+
assert_sanitized %(<a href="/blah" data-method="post">foo</a>), %(<a href="/blah">foo</a>)
464+
assert_sanitized %(<a data-remote="true" data-type="script" data-method="get" data-cross-domain="true" href="attack.js">Launch the missiles</a>), %(<a href="attack.js">Launch the missiles</a>)
465+
end
466+
467+
def test_allow_data_attribute_if_requested
468+
text = %(<a data-foo="foo">foo</a>)
469+
assert_equal %(<a data-foo="foo">foo</a>), white_list_sanitize(text, attributes: ['data-foo'])
470+
end
471+
456472
protected
457473

458474
def xpath_sanitize(input, options = {})
@@ -484,18 +500,18 @@ def sanitize_css(input)
484500
end
485501

486502
def scope_allowed_tags(tags)
503+
old_tags = Rails::Html::WhiteListSanitizer.allowed_tags
487504
Rails::Html::WhiteListSanitizer.allowed_tags = tags
488505
yield Rails::Html::WhiteListSanitizer.new
489-
490506
ensure
491-
Rails::Html::WhiteListSanitizer.allowed_tags = nil
507+
Rails::Html::WhiteListSanitizer.allowed_tags = old_tags
492508
end
493509

494510
def scope_allowed_attributes(attributes)
511+
old_attributes = Rails::Html::WhiteListSanitizer.allowed_attributes
495512
Rails::Html::WhiteListSanitizer.allowed_attributes = attributes
496513
yield Rails::Html::WhiteListSanitizer.new
497-
498514
ensure
499-
Rails::Html::WhiteListSanitizer.allowed_attributes = nil
515+
Rails::Html::WhiteListSanitizer.allowed_attributes = old_attributes
500516
end
501517
end

0 commit comments

Comments
 (0)