Skip to content

Commit 1c60d07

Browse files
committed
[1.4.x] Restrict the XML deserializer to prevent network and entity-expansion DoS attacks.
This is a security fix. Disclosure and advisory coming shortly.
1 parent 9936fdb commit 1c60d07

File tree

2 files changed

+108
-1
lines changed

2 files changed

+108
-1
lines changed

django/core/serializers/xml_serializer.py

+94-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
from django.utils.xmlutils import SimplerXMLGenerator
99
from django.utils.encoding import smart_unicode
1010
from xml.dom import pulldom
11+
from xml.sax import handler
12+
from xml.sax.expatreader import ExpatParser as _ExpatParser
1113

1214
class Serializer(base.Serializer):
1315
"""
@@ -149,9 +151,13 @@ class Deserializer(base.Deserializer):
149151

150152
def __init__(self, stream_or_string, **options):
151153
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())
153155
self.db = options.pop('using', DEFAULT_DB_ALIAS)
154156

157+
def _make_parser(self):
158+
"""Create a hardened XML parser (no custom/external entities)."""
159+
return DefusedExpatParser()
160+
155161
def next(self):
156162
for event, node in self.event_stream:
157163
if event == "START_ELEMENT" and node.nodeName == "object":
@@ -290,3 +296,90 @@ def getInnerText(node):
290296
else:
291297
pass
292298
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)

tests/regressiontests/serializers_regress/tests.py

+14
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from cStringIO import StringIO
1717
except ImportError:
1818
from StringIO import StringIO
19+
from django.core.serializers.xml_serializer import DTDForbidden
1920

2021
try:
2122
import yaml
@@ -523,3 +524,16 @@ def streamTest(format, self):
523524
if format != 'python':
524525
setattr(SerializerTests, 'test_' + format + '_serializer_stream', curry(streamTest, format))
525526

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 commit comments

Comments
 (0)