Permalink
Browse files

CVE-2013-0156: Safe XML params parsing. Doesn't allow symbols or yaml.

  • Loading branch information...
1 parent 5b8db45 commit 70adb9613e4a40c5645c99da374639c41012e4fc @jeremy jeremy committed with tenderlove Jan 6, 2013
@@ -121,6 +121,19 @@ def test_post_xml_using_an_attributted_node_named_type
end
end
+ def test_post_xml_using_a_disallowed_type_attribute
+ $stderr = StringIO.new
+ with_test_route_set do
+ post '/', '<foo type="symbol">value</foo>', 'CONTENT_TYPE' => 'application/xml'
+ assert_response 500
+
+ post '/', '<foo type="yaml">value</foo>', 'CONTENT_TYPE' => 'application/xml'
+ assert_response 500
+ end
+ ensure
+ $stderr = STDERR
+ end
+
def test_register_and_use_yaml
with_test_route_set do
ActionController::Base.param_parsers[Mime::YAML] = Proc.new { |d| YAML.load(d) }
@@ -1,5 +1,11 @@
+## Rails 2.3.15 (Jan 8, 2012) ##
+
+* Hash.from_xml raises when it encounters type="symbol" or type="yaml". Use Hash.from_trusted_xml to parse this XML. CVE-2013-0156 [Jeremy Kemper]
+
+
*2.3.11 (February 9, 2011)*
+
*2.3.10 (October 15, 2010)*
@@ -26,6 +26,13 @@ def content_type
end
end
+ DISALLOWED_XML_TYPES = %w(symbol yaml)
+ class DisallowedType < StandardError #:nodoc:
+ def initialize(type)
+ super "Disallowed type attribute: #{type.inspect}"
+ end
+ end
+
XML_TYPE_NAMES = {
"Symbol" => "symbol",
"Fixnum" => "integer",
@@ -160,24 +167,34 @@ def rename_key(key, options = {})
end
module ClassMethods
- def from_xml(xml)
- typecast_xml_value(unrename_keys(XmlMini.parse(xml)))
+ def from_xml(xml, disallowed_types = nil)
+ typecast_xml_value(unrename_keys(XmlMini.parse(xml)), disallowed_types)
+ end
+
+ def from_trusted_xml(xml)
+ from_xml xml, []
end
private
- def typecast_xml_value(value)
+ def typecast_xml_value(value, disallowed_types = nil)
+ disallowed_types ||= DISALLOWED_XML_TYPES
+
case value.class.to_s
when 'Hash'
+ if value.include?('type') && !value['type'].is_a?(Hash) && disallowed_types.include?(value['type'])
+ raise DisallowedType, value['type']
+ end
+
if value['type'] == 'array'
child_key, entries = value.detect { |k,v| k != 'type' } # child_key is throwaway
if entries.nil? || (c = value['__content__'] && c.blank?)
[]
else
case entries.class.to_s # something weird with classes not matching here. maybe singleton methods breaking is_a?
when "Array"
- entries.collect { |v| typecast_xml_value(v) }
+ entries.collect { |v| typecast_xml_value(v, disallowed_types) }
when "Hash"
- [typecast_xml_value(entries)]
+ [typecast_xml_value(entries, disallowed_types)]
else
raise "can't typecast #{entries.inspect}"
end
@@ -205,7 +222,7 @@ def typecast_xml_value(value)
nil
else
xml_value = value.inject({}) do |h,(k,v)|
- h[k] = typecast_xml_value(v)
+ h[k] = typecast_xml_value(v, disallowed_types)
h
end
@@ -214,7 +231,7 @@ def typecast_xml_value(value)
xml_value["file"].is_a?(StringIO) ? xml_value["file"] : xml_value
end
when 'Array'
- value.map! { |i| typecast_xml_value(i) }
+ value.map! { |i| typecast_xml_value(i, disallowed_types) }
case value.length
when 0 then nil
when 1 then value.first
@@ -575,12 +575,10 @@ def test_single_record_from_xml
<replies-close-in type="integer">2592000000</replies-close-in>
<written-on type="date">2003-07-16</written-on>
<viewed-at type="datetime">2003-07-16T09:28:00+0000</viewed-at>
- <content type="yaml">--- \n1: should be an integer\n:message: Have a nice day\narray: \n- should-have-dashes: true\n should_have_underscores: true\n</content>
<author-email-address>david@loudthinking.com</author-email-address>
<parent-id></parent-id>
<ad-revenue type="decimal">1.5</ad-revenue>
<optimum-viewing-angle type="float">135</optimum-viewing-angle>
- <resident type="symbol">yes</resident>
</topic>
EOT
@@ -593,12 +591,10 @@ def test_single_record_from_xml
:replies_close_in => 2592000000,
:written_on => Date.new(2003, 7, 16),
:viewed_at => Time.utc(2003, 7, 16, 9, 28),
- :content => { :message => "Have a nice day", 1 => "should be an integer", "array" => [{ "should-have-dashes" => true, "should_have_underscores" => true }] },
:author_email_address => "david@loudthinking.com",
:parent_id => nil,
:ad_revenue => BigDecimal("1.50"),
:optimum_viewing_angle => 135.0,
- :resident => :yes
}.stringify_keys
assert_equal expected_topic_hash, Hash.from_xml(topic_xml)["topic"]
@@ -612,7 +608,6 @@ def test_single_record_from_xml_with_nil_values
<approved type="boolean"></approved>
<written-on type="date"></written-on>
<viewed-at type="datetime"></viewed-at>
- <content type="yaml"></content>
<parent-id></parent-id>
</topic>
EOT
@@ -623,7 +618,6 @@ def test_single_record_from_xml_with_nil_values
:approved => nil,
:written_on => nil,
:viewed_at => nil,
- :content => nil,
:parent_id => nil
}.stringify_keys
@@ -833,6 +827,28 @@ def test_type_trickles_through_when_unknown
assert_equal expected_product_hash, Hash.from_xml(product_xml)["product"]
end
+ def test_from_xml_raises_on_disallowed_type_attributes
+ assert_raise Hash::DisallowedType do
+ Hash.from_xml '<product><name type="foo">value</name></product>', %w(foo)
+ end
+ end
+
+ def test_from_xml_disallows_symbol_and_yaml_types_by_default
+ assert_raise Hash::DisallowedType do
+ Hash.from_xml '<product><name type="symbol">value</name></product>'
+ end
+
+ assert_raise Hash::DisallowedType do
+ Hash.from_xml '<product><name type="yaml">value</name></product>'
+ end
+ end
+
+ def test_from_trusted_xml_allows_symbol_and_yaml_types
+ expected = { 'product' => { 'name' => :value }}
+ assert_equal expected, Hash.from_trusted_xml('<product><name type="symbol">value</name></product>')
+ assert_equal expected, Hash.from_trusted_xml('<product><name type="yaml">:value</name></product>')
+ end
+
def test_should_use_default_value_for_unknown_key
hash_wia = HashWithIndifferentAccess.new(3)
assert_equal 3, hash_wia[:new_key]
@@ -867,7 +883,7 @@ def test_kernel_method_names_to_xml
def test_empty_string_works_for_typecast_xml_value
assert_nothing_raised do
- Hash.__send__(:typecast_xml_value, "")
+ Hash.__send__(:typecast_xml_value, "", [])
end
end

0 comments on commit 70adb96

Please sign in to comment.