티스토리 수익 글 보기

티스토리 수익 글 보기

Restrict the XML deserializer to prevent network and entity-expansion… · django/django@c6d69c1 · GitHub
Skip to content

Commit c6d69c1

Browse files
committed
Restrict the XML deserializer to prevent network and entity-expansion DoS attacks.
This is a security fix. Disclosure and advisory coming shortly.
1 parent d51fb74 commit c6d69c1

File tree

2 files changed

+109
1
lines changed

2 files changed

+109
1
lines changed

django/core/serializers/xml_serializer.py

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
from django.utils.xmlutils import SimplerXMLGenerator
1111
from django.utils.encoding import smart_text
1212
from xml.dom import pulldom
13+
from xml.sax import handler
14+
from xml.sax.expatreader import ExpatParser as _ExpatParser
1315

1416
class Serializer(base.Serializer):
1517
"""
@@ -151,9 +153,13 @@ class Deserializer(base.Deserializer):
151153

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

159+
def _make_parser(self):
160+
"""Create a hardened XML parser (no custom/external entities)."""
161+
return DefusedExpatParser()
162+
157163
def __next__(self):
158164
for event, node in self.event_stream:
159165
if event == "START_ELEMENT" and node.nodeName == "object":
@@ -292,3 +298,90 @@ def getInnerText(node):
292298
else:
293299
pass
294300
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)

tests/regressiontests/serializers_regress/tests.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import datetime
1212
import decimal
13+
from django.core.serializers.xml_serializer import DTDForbidden
1314

1415
try:
1516
import yaml
@@ -514,3 +515,17 @@ def streamTest(format, self):
514515
if format != 'python':
515516
setattr(SerializerTests, 'test_' + format + '_serializer_stream', curry(streamTest, format))
516517

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

Comments
 (0)