Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

[1.5.x] Restricted the XML deserializer to prevent DoS attacks.

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

0 notes on commit 2d0c22e

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