Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

[1.4.x] Restrict the XML deserializer to prevent network and entity-e…

…xpansion DoS attacks.

This is a security fix. Disclosure and advisory coming shortly.
  • Loading branch information...
commit 1c60d07ba23e0350351c278ad28d0bd5aa410b40 1 parent 9936fdb
Carl Meyer authored February 11, 2013
95  django/core/serializers/xml_serializer.py
@@ -8,6 +8,8 @@
8 8
 from django.utils.xmlutils import SimplerXMLGenerator
9 9
 from django.utils.encoding import smart_unicode
10 10
 from xml.dom import pulldom
  11
+from xml.sax import handler
  12
+from xml.sax.expatreader import ExpatParser as _ExpatParser
11 13
 
12 14
 class Serializer(base.Serializer):
13 15
     """
@@ -149,9 +151,13 @@ class Deserializer(base.Deserializer):
149 151
 
150 152
     def __init__(self, stream_or_string, **options):
151 153
         super(Deserializer, self).__init__(stream_or_string, **options)
152  
-        self.event_stream = pulldom.parse(self.stream)
  154
+        self.event_stream = pulldom.parse(self.stream, self._make_parser())
153 155
         self.db = options.pop('using', DEFAULT_DB_ALIAS)
154 156
 
  157
+    def _make_parser(self):
  158
+        """Create a hardened XML parser (no custom/external entities)."""
  159
+        return DefusedExpatParser()
  160
+
155 161
     def next(self):
156 162
         for event, node in self.event_stream:
157 163
             if event == "START_ELEMENT" and node.nodeName == "object":
@@ -290,3 +296,90 @@ def getInnerText(node):
290 296
         else:
291 297
            pass
292 298
     return u"".join(inner_text)
  299
+
  300
+
  301
+# Below code based on Christian Heimes' defusedxml
  302
+
  303
+
  304
+class DefusedExpatParser(_ExpatParser):
  305
+    """
  306
+    An expat parser hardened against XML bomb attacks.
  307
+
  308
+    Forbids DTDs, external entity references
  309
+
  310
+    """
  311
+    def __init__(self, *args, **kwargs):
  312
+        _ExpatParser.__init__(self, *args, **kwargs)
  313
+        self.setFeature(handler.feature_external_ges, False)
  314
+        self.setFeature(handler.feature_external_pes, False)
  315
+
  316
+    def start_doctype_decl(self, name, sysid, pubid, has_internal_subset):
  317
+        raise DTDForbidden(name, sysid, pubid)
  318
+
  319
+    def entity_decl(self, name, is_parameter_entity, value, base,
  320
+                    sysid, pubid, notation_name):
  321
+        raise EntitiesForbidden(name, value, base, sysid, pubid, notation_name)
  322
+
  323
+    def unparsed_entity_decl(self, name, base, sysid, pubid, notation_name):
  324
+        # expat 1.2
  325
+        raise EntitiesForbidden(name, None, base, sysid, pubid, notation_name)
  326
+
  327
+    def external_entity_ref_handler(self, context, base, sysid, pubid):
  328
+        raise ExternalReferenceForbidden(context, base, sysid, pubid)
  329
+
  330
+    def reset(self):
  331
+        _ExpatParser.reset(self)
  332
+        parser = self._parser
  333
+        parser.StartDoctypeDeclHandler = self.start_doctype_decl
  334
+        parser.EntityDeclHandler = self.entity_decl
  335
+        parser.UnparsedEntityDeclHandler = self.unparsed_entity_decl
  336
+        parser.ExternalEntityRefHandler = self.external_entity_ref_handler
  337
+
  338
+
  339
+class DefusedXmlException(ValueError):
  340
+    """Base exception."""
  341
+    def __repr__(self):
  342
+        return str(self)
  343
+
  344
+
  345
+class DTDForbidden(DefusedXmlException):
  346
+    """Document type definition is forbidden."""
  347
+    def __init__(self, name, sysid, pubid):
  348
+        super(DTDForbidden, self).__init__()
  349
+        self.name = name
  350
+        self.sysid = sysid
  351
+        self.pubid = pubid
  352
+
  353
+    def __str__(self):
  354
+        tpl = "DTDForbidden(name='{}', system_id={!r}, public_id={!r})"
  355
+        return tpl.format(self.name, self.sysid, self.pubid)
  356
+
  357
+
  358
+class EntitiesForbidden(DefusedXmlException):
  359
+    """Entity definition is forbidden."""
  360
+    def __init__(self, name, value, base, sysid, pubid, notation_name):
  361
+        super(EntitiesForbidden, self).__init__()
  362
+        self.name = name
  363
+        self.value = value
  364
+        self.base = base
  365
+        self.sysid = sysid
  366
+        self.pubid = pubid
  367
+        self.notation_name = notation_name
  368
+
  369
+    def __str__(self):
  370
+        tpl = "EntitiesForbidden(name='{}', system_id={!r}, public_id={!r})"
  371
+        return tpl.format(self.name, self.sysid, self.pubid)
  372
+
  373
+
  374
+class ExternalReferenceForbidden(DefusedXmlException):
  375
+    """Resolving an external reference is forbidden."""
  376
+    def __init__(self, context, base, sysid, pubid):
  377
+        super(ExternalReferenceForbidden, self).__init__()
  378
+        self.context = context
  379
+        self.base = base
  380
+        self.sysid = sysid
  381
+        self.pubid = pubid
  382
+
  383
+    def __str__(self):
  384
+        tpl = "ExternalReferenceForbidden(system_id='{}', public_id={})"
  385
+        return tpl.format(self.sysid, self.pubid)
14  tests/regressiontests/serializers_regress/tests.py
@@ -16,6 +16,7 @@
16 16
     from cStringIO import StringIO
17 17
 except ImportError:
18 18
     from StringIO import StringIO
  19
+from django.core.serializers.xml_serializer import DTDForbidden
19 20
 
20 21
 try:
21 22
     import yaml
@@ -523,3 +524,16 @@ def streamTest(format, self):
523 524
     if format != 'python':
524 525
         setattr(SerializerTests, 'test_' + format + '_serializer_stream', curry(streamTest, format))
525 526
 
  527
+
  528
+class XmlDeserializerSecurityTests(TestCase):
  529
+
  530
+    def test_no_dtd(self):
  531
+        """
  532
+        The XML deserializer shouldn't allow a DTD.
  533
+
  534
+        This is the most straightforward way to prevent all entity definitions
  535
+        and avoid both external entities and entity-expansion attacks.
  536
+
  537
+        """
  538
+        xml = '<?xml version="1.0" standalone="no"?><!DOCTYPE example SYSTEM "http://example.com/example.dtd">'
  539
+        self.assertRaises(DTDForbidden, serializers.deserialize('xml', xml).next)

0 notes on commit 1c60d07

Please sign in to comment.
Something went wrong with that request. Please try again.