티스토리 수익 글 보기

티스토리 수익 글 보기

[1.3.x] Restrict the XML deserializer to prevent network and entity-e… · django/django@d19a270 · GitHub
Skip to content

Commit d19a270

Browse files
carljmaaugustin
authored andcommitted
[1.3.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 27cd872 commit d19a270

File tree

2 files changed

+108
1
lines changed

2 files changed

+108
1
lines changed

django/core/serializers/xml_serializer.py

Lines changed: 93 additions & 1 deletion
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
"""
@@ -154,9 +156,13 @@ class Deserializer(base.Deserializer):
154156

155157
def __init__(self, stream_or_string, **options):
156158
super(Deserializer, self).__init__(stream_or_string, **options)
157-
self.event_stream = pulldom.parse(self.stream)
159+
self.event_stream = pulldom.parse(self.stream, self._make_parser())
158160
self.db = options.pop('using', DEFAULT_DB_ALIAS)
159161

162+
def _make_parser(self):
163+
"""Create a hardened XML parser (no custom/external entities)."""
164+
return DefusedExpatParser()
165+
160166
def next(self):
161167
for event, node in self.event_stream:
162168
if event == "START_ELEMENT" and node.nodeName == "object":
@@ -295,3 +301,89 @@ def getInnerText(node):
295301
else:
296302
pass
297303
return u"".join(inner_text)
304+
305+
306+
# Below code based on Christian Heimes' defusedxml
307+
308+
309+
class DefusedExpatParser(_ExpatParser):
310+
"""
311+
An expat parser hardened against XML bomb attacks.
312+
313+
Forbids DTDs, external entity references
314+
315+
"""
316+
def __init__(self, *args, **kwargs):
317+
_ExpatParser.__init__(self, *args, **kwargs)
318+
self.setFeature(handler.feature_external_ges, False)
319+
self.setFeature(handler.feature_external_pes, False)
320+
321+
def start_doctype_decl(self, name, sysid, pubid, has_internal_subset):
322+
raise DTDForbidden(name, sysid, pubid)
323+
324+
def entity_decl(self, name, is_parameter_entity, value, base,
325+
sysid, pubid, notation_name):
326+
raise EntitiesForbidden(name, value, base, sysid, pubid, notation_name)
327+
328+
def unparsed_entity_decl(self, name, base, sysid, pubid, notation_name):
329+
# expat 1.2
330+
raise EntitiesForbidden(name, None, base, sysid, pubid, notation_name)
331+
332+
def external_entity_ref_handler(self, context, base, sysid, pubid):
333+
raise ExternalReferenceForbidden(context, base, sysid, pubid)
334+
335+
def reset(self):
336+
_ExpatParser.reset(self)
337+
parser = self._parser
338+
parser.StartDoctypeDeclHandler = self.start_doctype_decl
339+
parser.EntityDeclHandler = self.entity_decl
340+
parser.UnparsedEntityDeclHandler = self.unparsed_entity_decl
341+
parser.ExternalEntityRefHandler = self.external_entity_ref_handler
342+
343+
344+
class DefusedXmlException(ValueError):
345+
"""Base exception."""
346+
def __repr__(self):
347+
return str(self)
348+
349+
350+
class DTDForbidden(DefusedXmlException):
351+
"""Document type definition is forbidden."""
352+
def __init__(self, name, sysid, pubid):
353+
self.name = name
354+
self.sysid = sysid
355+
self.pubid = pubid
356+
357+
def __str__(self):
358+
tpl = "DTDForbidden(name='{}', system_id={!r}, public_id={!r})"
359+
return tpl.format(self.name, self.sysid, self.pubid)
360+
361+
362+
class EntitiesForbidden(DefusedXmlException):
363+
"""Entity definition is forbidden."""
364+
def __init__(self, name, value, base, sysid, pubid, notation_name):
365+
super(EntitiesForbidden, self).__init__()
366+
self.name = name
367+
self.value = value
368+
self.base = base
369+
self.sysid = sysid
370+
self.pubid = pubid
371+
self.notation_name = notation_name
372+
373+
def __str__(self):
374+
tpl = "EntitiesForbidden(name='{}', system_id={!r}, public_id={!r})"
375+
return tpl.format(self.name, self.sysid, self.pubid)
376+
377+
378+
class ExternalReferenceForbidden(DefusedXmlException):
379+
"""Resolving an external reference is forbidden."""
380+
def __init__(self, context, base, sysid, pubid):
381+
super(ExternalReferenceForbidden, self).__init__()
382+
self.context = context
383+
self.base = base
384+
self.sysid = sysid
385+
self.pubid = pubid
386+
387+
def __str__(self):
388+
tpl = "ExternalReferenceForbidden(system_id='{}', public_id={})"
389+
return tpl.format(self.sysid, self.pubid)

tests/regressiontests/serializers_regress/tests.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from cStringIO import StringIO
1515
except ImportError:
1616
from StringIO import StringIO
17+
from django.core.serializers.xml_serializer import DTDForbidden
1718

1819
from django.conf import settings
1920
from django.core import serializers, management
@@ -416,3 +417,17 @@ def streamTest(format, self):
416417
setattr(SerializerTests, 'test_' + format + '_serializer_fields', curry(fieldsTest, format))
417418
if format != 'python':
418419
setattr(SerializerTests, 'test_' + format + '_serializer_stream', curry(streamTest, format))
420+
421+
422+
class XmlDeserializerSecurityTests(TestCase):
423+
424+
def test_no_dtd(self):
425+
"""
426+
The XML deserializer shouldn't allow a DTD.
427+
428+
This is the most straightforward way to prevent all entity definitions
429+
and avoid both external entities and entity-expansion attacks.
430+
431+
"""
432+
xml = '<?xml version="1.0" standalone="no"?><!DOCTYPE example SYSTEM "http://example.com/example.dtd">'
433+
self.assertRaises(DTDForbidden, serializers.deserialize('xml', xml).next)

0 commit comments

Comments
 (0)