Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Restrict the XML deserializer to prevent network and entity-expansion…

… DoS attacks.

This is a security fix. Disclosure and advisory coming shortly.
  • Loading branch information...
commit c6d69c12ea7ee9ad35abc7dbf95e00d624d0df5d 1 parent d51fb74
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
@@ -514,3 +515,17 @@ def streamTest(format, self):
514 515
     if format != 'python':
515 516
         setattr(SerializerTests, 'test_' + format + '_serializer_stream', curry(streamTest, format))
516 517
 
  518
+
  519
+class XmlDeserializerSecurityTests(TestCase):
  520
+
  521
+    def test_no_dtd(self):
  522
+        """
  523
+        The XML deserializer shouldn't allow a DTD.
  524
+
  525
+        This is the most straightforward way to prevent all entity definitions
  526
+        and avoid both external entities and entity-expansion attacks.
  527
+
  528
+        """
  529
+        xml = '<?xml version="1.0" standalone="no"?><!DOCTYPE example SYSTEM "http://example.com/example.dtd">'
  530
+        with self.assertRaises(DTDForbidden):
  531
+            next(serializers.deserialize('xml', xml))

0 notes on commit c6d69c1

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