Skip to content

Commit 2392535

Browse files
committed
Merge branch 'master-sec'
* master-sec: fix protocol checking in sanitization [CVE-2013-1857] JDOM XXE Protection [CVE-2013-1856] fix incorrect ^$ usage leading to XSS in sanitize_css [CVE-2013-1855] stop calling to_sym when building arel nodes [CVE-2013-1854]
2 parents 0053c21 + e115ace commit 2392535

File tree

9 files changed

+81
-18
lines changed

9 files changed

+81
-18
lines changed

actionpack/lib/action_view/vendor/html-scanner/html/sanitizer.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ class WhiteListSanitizer < Sanitizer
7777

7878
# A regular expression of the valid characters used to separate protocols like
7979
# the ':' in 'http://foo.com'
80-
self.protocol_separator = /:|(&#0*58)|(&#x70)|(%|&#37;)3A/
80+
self.protocol_separator = /:|(&#0*58)|(&#x70)|(&#x0*3a)|(%|&#37;)3A/i
8181

8282
# Specifies a Set of HTML attributes that can have URIs.
8383
self.uri_attributes = Set.new(%w(href src cite action longdesc xlink:href lowsrc))
@@ -121,8 +121,8 @@ def sanitize_css(style)
121121
style = style.to_s.gsub(/url\s*\(\s*[^\s)]+?\s*\)\s*/, ' ')
122122

123123
# gauntlet
124-
if style !~ /^([:,;#%.\sa-zA-Z0-9!]|\w-\w|\'[\s\w]+\'|\"[\s\w]+\"|\([\d,\s]+\))*$/ ||
125-
style !~ /^(\s*[-\w]+\s*:\s*[^:;]*(;|$)\s*)*$/
124+
if style !~ /\A([:,;#%.\sa-zA-Z0-9!]|\w-\w|\'[\s\w]+\'|\"[\s\w]+\"|\([\d,\s]+\))*\z/ ||
125+
style !~ /\A(\s*[-\w]+\s*:\s*[^:;]*(;|$)\s*)*\z/
126126
return ''
127127
end
128128

@@ -133,7 +133,7 @@ def sanitize_css(style)
133133
elsif shorthand_css_properties.include?(prop.split('-')[0].downcase)
134134
unless val.split().any? do |keyword|
135135
!allowed_css_keywords.include?(keyword) &&
136-
keyword !~ /^(#[0-9a-f]+|rgb\(\d+%?,\d*%?,?\d*%?\)?|\d{0,2}\.?\d{0,2}(cm|em|ex|in|mm|pc|pt|px|%|,|\))?)$/
136+
keyword !~ /\A(#[0-9a-f]+|rgb\(\d+%?,\d*%?,?\d*%?\)?|\d{0,2}\.?\d{0,2}(cm|em|ex|in|mm|pc|pt|px|%|,|\))?)\z/
137137
end
138138
clean << prop + ': ' + val + ';'
139139
end
@@ -182,7 +182,7 @@ def process_attributes_for(node, options)
182182

183183
def contains_bad_protocols?(attr_name, value)
184184
uri_attributes.include?(attr_name) &&
185-
(value =~ /(^[^\/:]*):|(&#0*58)|(&#x70)|(%|&#37;)3A/ && !allowed_protocols.include?(value.split(protocol_separator).first.downcase.strip))
185+
(value =~ /(^[^\/:]*):|(&#0*58)|(&#x70)|(&#x0*3a)|(%|&#37;)3A/i && !allowed_protocols.include?(value.split(protocol_separator).first.downcase.strip))
186186
end
187187
end
188188
end

actionpack/test/template/html-scanner/sanitizer_test.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ def test_should_block_script_tag
200200
%(<IMG SRC="jav&#x0A;ascript:alert('XSS');">),
201201
%(<IMG SRC="jav&#x0D;ascript:alert('XSS');">),
202202
%(<IMG SRC=" &#14; javascript:alert('XSS');">),
203+
%(<IMG SRC="javascript&#x3a;alert('XSS');">),
203204
%(<IMG SRC=`javascript:alert("RSnake says, 'XSS'")`>)].each_with_index do |img_hack, i|
204205
define_method "test_should_not_fall_for_xss_image_hack_#{i+1}" do
205206
assert_sanitized img_hack, "<img>"
@@ -279,6 +280,11 @@ def test_should_sanitize_div_style_expression
279280
assert_equal '', sanitize_css(raw)
280281
end
281282

283+
def test_should_sanitize_across_newlines
284+
raw = %(\nwidth:\nexpression(alert('XSS'));\n)
285+
assert_equal '', sanitize_css(raw)
286+
end
287+
282288
def test_should_sanitize_img_vbscript
283289
assert_sanitized %(<img src='vbscript:msgbox("XSS")' />), '<img />'
284290
end
@@ -299,6 +305,15 @@ def test_should_sanitize_neverending_attribute
299305
assert_sanitized "<span class=\"\\", "<span class=\"\\\">"
300306
end
301307

308+
def test_x03a
309+
assert_sanitized %(<a href="javascript&#x3a;alert('XSS');">), "<a>"
310+
assert_sanitized %(<a href="javascript&#x003a;alert('XSS');">), "<a>"
311+
assert_sanitized %(<a href="http&#x3a;//legit">), %(<a href="http://legit">)
312+
assert_sanitized %(<a href="javascript&#x3A;alert('XSS');">), "<a>"
313+
assert_sanitized %(<a href="javascript&#x003A;alert('XSS');">), "<a>"
314+
assert_sanitized %(<a href="http&#x3A;//legit">), %(<a href="http://legit">)
315+
end
316+
302317
protected
303318
def assert_sanitized(input, expected = nil)
304319
@sanitizer ||= HTML::WhiteListSanitizer.new

activerecord/lib/active_record/relation/predicate_builder.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def self.expand(klass, table, column, value)
4848
column = reflection.foreign_key
4949
end
5050

51-
queries << build(table[column.to_sym], value)
51+
queries << build(table[column], value)
5252
queries
5353
end
5454

activerecord/test/cases/relation/where_chain_test.rb

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,47 +6,52 @@ module ActiveRecord
66
class WhereChainTest < ActiveRecord::TestCase
77
fixtures :posts
88

9+
def setup
10+
super
11+
@name = 'title'
12+
end
13+
914
def test_not_eq
10-
expected = Arel::Nodes::NotEqual.new(Post.arel_table[:title], 'hello')
15+
expected = Arel::Nodes::NotEqual.new(Post.arel_table[@name], 'hello')
1116
relation = Post.where.not(title: 'hello')
1217
assert_equal([expected], relation.where_values)
1318
end
1419

1520
def test_not_null
16-
expected = Arel::Nodes::NotEqual.new(Post.arel_table[:title], nil)
21+
expected = Arel::Nodes::NotEqual.new(Post.arel_table[@name], nil)
1722
relation = Post.where.not(title: nil)
1823
assert_equal([expected], relation.where_values)
1924
end
2025

2126
def test_not_in
22-
expected = Arel::Nodes::NotIn.new(Post.arel_table[:title], %w[hello goodbye])
27+
expected = Arel::Nodes::NotIn.new(Post.arel_table[@name], %w[hello goodbye])
2328
relation = Post.where.not(title: %w[hello goodbye])
2429
assert_equal([expected], relation.where_values)
2530
end
2631

2732
def test_association_not_eq
28-
expected = Arel::Nodes::NotEqual.new(Comment.arel_table[:title], 'hello')
33+
expected = Arel::Nodes::NotEqual.new(Comment.arel_table[@name], 'hello')
2934
relation = Post.joins(:comments).where.not(comments: {title: 'hello'})
3035
assert_equal(expected.to_sql, relation.where_values.first.to_sql)
3136
end
3237

3338
def test_not_eq_with_preceding_where
3439
relation = Post.where(title: 'hello').where.not(title: 'world')
3540

36-
expected = Arel::Nodes::Equality.new(Post.arel_table[:title], 'hello')
41+
expected = Arel::Nodes::Equality.new(Post.arel_table[@name], 'hello')
3742
assert_equal(expected, relation.where_values.first)
3843

39-
expected = Arel::Nodes::NotEqual.new(Post.arel_table[:title], 'world')
44+
expected = Arel::Nodes::NotEqual.new(Post.arel_table[@name], 'world')
4045
assert_equal(expected, relation.where_values.last)
4146
end
4247

4348
def test_not_eq_with_succeeding_where
4449
relation = Post.where.not(title: 'hello').where(title: 'world')
4550

46-
expected = Arel::Nodes::NotEqual.new(Post.arel_table[:title], 'hello')
51+
expected = Arel::Nodes::NotEqual.new(Post.arel_table[@name], 'hello')
4752
assert_equal(expected, relation.where_values.first)
4853

49-
expected = Arel::Nodes::Equality.new(Post.arel_table[:title], 'world')
54+
expected = Arel::Nodes::Equality.new(Post.arel_table[@name], 'world')
5055
assert_equal(expected, relation.where_values.last)
5156
end
5257

@@ -65,10 +70,10 @@ def test_not_eq_with_array_parameter
6570
def test_chaining_multiple
6671
relation = Post.where.not(author_id: [1, 2]).where.not(title: 'ruby on rails')
6772

68-
expected = Arel::Nodes::NotIn.new(Post.arel_table[:author_id], [1, 2])
73+
expected = Arel::Nodes::NotIn.new(Post.arel_table['author_id'], [1, 2])
6974
assert_equal(expected, relation.where_values[0])
7075

71-
expected = Arel::Nodes::NotEqual.new(Post.arel_table[:title], 'ruby on rails')
76+
expected = Arel::Nodes::NotEqual.new(Post.arel_table[@name], 'ruby on rails')
7277
assert_equal(expected, relation.where_values[1])
7378
end
7479
end

activesupport/lib/active_support/xml_mini/jdom.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ def parse(data)
3737
{}
3838
else
3939
@dbf = DocumentBuilderFactory.new_instance
40+
# secure processing of java xml
41+
# http://www.ibm.com/developerworks/xml/library/x-tipcfsx/index.html
42+
@dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false)
43+
@dbf.setFeature("http://xml.org/sax/features/external-general-entities", false)
44+
@dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false)
45+
@dbf.setFeature(javax.xml.XMLConstants::FEATURE_SECURE_PROCESSING, true)
4046
xml_string_reader = StringReader.new(data)
4147
xml_input_source = InputSource.new(xml_string_reader)
4248
doc = @dbf.new_document_builder.parse(xml_input_source)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<!ENTITY a "external entity">
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<!ENTITY a "hello">
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
include me

activesupport/test/xml_mini/jdom_engine_test.rb

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
33
require 'active_support/xml_mini'
44
require 'active_support/core_ext/hash/conversions'
55

6+
67
class JDOMEngineTest < ActiveSupport::TestCase
78
include ActiveSupport
89

10+
FILES_DIR = File.dirname(__FILE__) + '/../fixtures/xml'
11+
912
def setup
1013
@default_backend = XmlMini.backend
1114
XmlMini.backend = 'JDOM'
@@ -30,10 +33,41 @@ def test_file_from_xml
3033
assert_equal 'image/png', file.content_type
3134
end
3235

36+
def test_not_allowed_to_expand_entities_to_files
37+
attack_xml = <<-EOT
38+
<!DOCTYPE member [
39+
<!ENTITY a SYSTEM "file://#{FILES_DIR}/jdom_include.txt">
40+
]>
41+
<member>x&a;</member>
42+
EOT
43+
assert_equal 'x', Hash.from_xml(attack_xml)["member"]
44+
end
45+
46+
def test_not_allowed_to_expand_parameter_entities_to_files
47+
attack_xml = <<-EOT
48+
<!DOCTYPE member [
49+
<!ENTITY % b SYSTEM "file://#{FILES_DIR}/jdom_entities.txt">
50+
%b;
51+
]>
52+
<member>x&a;</member>
53+
EOT
54+
assert_raise Java::OrgXmlSax::SAXParseException do
55+
assert_equal 'x', Hash.from_xml(attack_xml)["member"]
56+
end
57+
end
58+
59+
60+
def test_not_allowed_to_load_external_doctypes
61+
attack_xml = <<-EOT
62+
<!DOCTYPE member SYSTEM "file://#{FILES_DIR}/jdom_doctype.dtd">
63+
<member>x&a;</member>
64+
EOT
65+
assert_equal 'x', Hash.from_xml(attack_xml)["member"]
66+
end
67+
3368
def test_exception_thrown_on_expansion_attack
34-
assert_raise NativeException do
69+
assert_raise Java::OrgXmlSax::SAXParseException do
3570
attack_xml = <<-EOT
36-
<?xml version="1.0" encoding="UTF-8"?>
3771
<!DOCTYPE member [
3872
<!ENTITY a "&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;">
3973
<!ENTITY b "&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;">

0 commit comments

Comments
 (0)