Skip to content

Commit 13b7396

Browse files
committed
issue2551116 - Replace xmlrpclib (xmlrpc.client) with defusedxml.
defusedxml will be used to moneypatch the problematic client and server modules. Test added using an xml bomb.
1 parent 3dbb108 commit 13b7396

File tree

7 files changed

+121
-2
lines changed

7 files changed

+121
-2
lines changed

CHANGES.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ Features:
8181
Rouillard)
8282
- added fuzz testing for some code. Found issue2551382 and
8383
others. (John Rouillard)
84+
- issue2551116 - Replace xmlrpclib (xmlrpc.client) with defusedxml.
85+
Added support for defusedxml to better secure the xmlrpc
86+
endpoint. (John Rouillard)
8487

8588
2024-07-13 2.4.0
8689

doc/installation.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,11 @@ jinja2
255255
its TEMPLATE-INFO.txt file) you need
256256
to have the jinja2_ template engine installed.
257257

258+
defusedxml
259+
If you are going to enable and use the XMLRPC endpoint, you should
260+
install the defusedxml_ module. It will still work with the default
261+
xmlrpc standard library, but it will log a warning when used.
262+
258263
.. _install/docutils:
259264

260265
docutils
@@ -2371,6 +2376,7 @@ the test.
23712376
.. _apache: https://httpd.apache.org/
23722377
.. _brotli: https://pypi.org/project/Brotli/
23732378
.. _`developer's guide`: developers.html
2379+
.. _defusedxml: https://pypi.org/project/defusedxml/
23742380
.. _docutils: https://pypi.org/project/docutils/
23752381
.. _flup: https://pypi.org/project/flup/
23762382
.. _gpg: https://www.gnupg.org/software/gpgme/index.html

doc/upgrading.txt

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,36 @@ following diff::
159159
add the lines marked with ``+`` in the file in the location after
160160
check_main is assigned.
161161

162+
Defusedxml support improves XMLRPC security (optional)
163+
------------------------------------------------------
164+
165+
This release adds support for the defusedxml_ module. If it is
166+
installed it will be automatically used. The default xmlrpc module in
167+
the standard library has known issues when parsing crafted XML. It can
168+
take a lot of CPU time and consume large amounts of memory with small
169+
payloads.
170+
171+
When the XMLRPC endpoint is used without defusedxml, it will log a
172+
warning to the log file. The log entry can be disabled by adding::
173+
174+
175+
from roundup.cgi import client
176+
client.WARN_FOR_MISSING_DEFUSEDXML = False
177+
178+
to the ``interfaces.py`` file in the tracker home. (Create the file if
179+
it is missing.)
180+
181+
XMLRPC access is enabled by default in the classic and other trackers.
182+
Upgrading to defusedxml is considered optional because the XMLRPC
183+
endpoint can be disabled in the tracker's ``config.ini``. Also
184+
``Xmlrpc Access`` can be removed from the ``Users`` role by commenting
185+
out a line in ``schema.py``.
186+
187+
If you have enabled the xmlrpc endpoint, you should install
188+
defusedxml.
189+
190+
.. _defusedxml: https://pypi.org/project/defusedxml/
191+
162192
More secure session cookie handling (info)
163193
------------------------------------------
164194

doc/xmlrpc.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,8 @@ Both the standalone and embedded roundup XML endpoints used the
7979
default python XML parser. This parser is know to have security
8080
issues. For details see: https://pypi.org/project/defusedxml/.
8181
You may wish to use the rest interface which doesn't have the same
82-
issues. Patches with tests to roundup to use defusedxml are welcome.
82+
issues. If you install defusedxml, it will be automatically used to add
83+
some additional protection.
8384

8485
.. caution::
8586

roundup/anypy/xmlrpc_.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
11
try:
22
# Python 3+.
33
from xmlrpc import client, server
4+
# If client.defusedxml == False, client.py will warn that
5+
# xmlrpc is insecure and defusedxml should be installed.
6+
client.defusedxml=False
7+
try:
8+
from defusedxml import xmlrpc
9+
xmlrpc.monkey_patch()
10+
# figure out how to allow user to set xmlrpc.MAX_DATA = bytes
11+
client.defusedxml=True
12+
except ImportError:
13+
# use regular xmlrpc with warnings
14+
pass
15+
416
server.SimpleXMLRPCDispatcher
517
except (ImportError, AttributeError):
618
# Python 2.

roundup/cgi/client.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@ def add_message(msg_list, msg, escape=True):
101101
msg_list.append(msg)
102102
return msg_list # for unittests
103103

104+
# if set to False via interfaces.py do not log a warning when
105+
# xmlrpc is used and defusedxml is not installed.
106+
WARN_FOR_MISSING_DEFUSEDXML = True
104107

105108
default_err_msg = ''"""<html><head><title>An error has occurred</title></head>
106109
<body><h1>An error has occurred</h1>
@@ -656,6 +659,8 @@ def handle_xmlrpc(self):
656659
csrf_ok = False # we had an error, failed check
657660

658661
if csrf_ok is True:
662+
if WARN_FOR_MISSING_DEFUSEDXML and (not xmlrpc_.client.defusedxml):
663+
logger.warning(self._("XMLRPC endpoint is not using defusedxml. Improve security by installing defusedxml."))
659664
handler = xmlrpc.RoundupDispatcher(self.db,
660665
self.instance.actions,
661666
self.translator,

test/test_xmlrpc.py

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
#
66

77
from __future__ import print_function
8-
import unittest, os, shutil, errno, sys, difflib, re
8+
import unittest, os, shutil, errno, pytest, sys, difflib, re
99

1010
from roundup.anypy import xmlrpc_
1111
MultiCall = xmlrpc_.client.MultiCall
@@ -21,6 +21,19 @@
2121
from .test_mysql import skip_mysql
2222
from .test_postgresql import skip_postgresql
2323

24+
from .pytest_patcher import mark_class
25+
from roundup.anypy.xmlrpc_ import client
26+
27+
if client.defusedxml:
28+
skip_defusedxml = lambda func, *args, **kwargs: func
29+
30+
skip_defusedxml_used = mark_class(pytest.mark.skip(
31+
reason='Skipping non-defusedxml tests: defusedxml library in use'))
32+
else:
33+
skip_defusedxml = mark_class(pytest.mark.skip(
34+
reason='Skipping defusedxml tests: defusedxml library not available'))
35+
36+
skip_defusedxml_used = lambda func, *args, **kwargs: func
2437

2538
class XmlrpcTest(object):
2639

@@ -314,6 +327,55 @@ class S:
314327
for n, r in enumerate(result):
315328
self.assertEqual(r, results[n])
316329

330+
@skip_defusedxml
331+
def testDefusedXmlBomb(self):
332+
self.XmlBomb(expectIn=b"defusedxml.common.EntitiesForbidden")
333+
334+
@skip_defusedxml_used
335+
def testNonDefusedXmlBomb(self):
336+
self.XmlBomb(expectIn=b"1234567890"*511)
337+
338+
def XmlBomb(self, expectIn=None):
339+
340+
bombInput = """<?xml version='1.0'?>
341+
<!DOCTYPE xmlbomb [
342+
<!ENTITY a "1234567890" >
343+
<!ENTITY b "&a;&a;&a;&a;&a;&a;&a;&a;">
344+
<!ENTITY c "&b;&b;&b;&b;&b;&b;&b;&b;">
345+
<!ENTITY d "&c;&c;&c;&c;&c;&c;&c;&c;">
346+
]>
347+
<methodCall>
348+
<methodName>filter</methodName>
349+
<params>
350+
<param>
351+
<value><string>&d;</string></value>
352+
</param>
353+
<param>
354+
<value><array><data>
355+
<value><string>0</string></value>
356+
<value><string>2</string></value>
357+
<value><string>3</string></value>
358+
</data></array></value>
359+
</param>
360+
<param>
361+
<value><struct>
362+
<member>
363+
<name>username</name>
364+
<value><string>demo</string></value>
365+
</member>
366+
</struct></value>
367+
</param>
368+
</params>
369+
</methodCall>
370+
"""
371+
translator = TranslationService.get_translation(
372+
language=self.instance.config["TRACKER_LANGUAGE"],
373+
tracker_home=self.instance.config["TRACKER_HOME"])
374+
self.server = RoundupDispatcher(self.db, self.instance.actions,
375+
translator, allow_none = True)
376+
response = self.server.dispatch(bombInput)
377+
print(response)
378+
self.assertIn(expectIn, response)
317379

318380
class anydbmXmlrpcTest(XmlrpcTest, unittest.TestCase):
319381
backend = 'anydbm'

0 commit comments

Comments
 (0)